lib/matcha.ex

defmodule Matcha do
  @readme "README.md"
  @external_resource @readme
  @moduledoc_blurb @readme
                   |> File.read!()
                   |> String.split("<!-- MODULEDOC BLURB -->")
                   |> Enum.fetch!(1)
  @moduledoc_snippet @readme
                     |> File.read!()
                     |> String.split("<!-- MODULEDOC SNIPPET -->")
                     |> Enum.fetch!(1)

  @moduledoc """
  #{@moduledoc_blurb}

  #{@moduledoc_snippet}
  """

  alias Matcha.Context
  alias Matcha.Rewrite

  alias Matcha.Pattern
  alias Matcha.Spec
  alias Matcha.Trace

  @default_context Matcha.Context.FilterMap

  @spec pattern(Macro.t()) :: Macro.t()
  @doc """
  Builds a `Matcha.Pattern` that represents a pattern matching operation on a given input.

  For more information on match patterns, consult the `Matcha.Pattern` docs.

  ## Examples

      iex> require Matcha
      ...> pattern = Matcha.pattern({x, y, x})
      #Matcha.Pattern<{:"$1", :"$2", :"$1"}>
      iex> Matcha.Pattern.match?(pattern, {1, 2, 3})
      false
      iex> Matcha.Pattern.match?(pattern, {1, 2, 1})
      true

  """
  defmacro pattern(pattern) do
    source =
      %Rewrite{env: __CALLER__, source: pattern}
      |> Rewrite.ast_to_pattern_source(pattern)

    quote location: :keep do
      %Pattern{source: unquote(source)}
      |> Pattern.validate!()
    end
  end

  defp do_spec(caller, context, clauses) do
    require Rewrite

    Enum.each(clauses, fn
      {:->, _, _} ->
        :ok

      other ->
        raise ArgumentError,
          message:
            "#{__MODULE__}.spec/2 must be provided with `->` clauses," <>
              " got: `#{Macro.to_string(other)}`"
    end)

    context =
      context
      |> Rewrite.perform_expansion(caller)
      |> Context.resolve()

    source =
      %Rewrite{env: caller, context: context, source: clauses}
      |> Rewrite.ast_to_spec_source(clauses)

    quote location: :keep do
      %Spec{source: unquote(source), context: unquote(context)}
      |> Spec.validate!()
    end
  end

  @spec spec(Context.t(), Macro.t()) :: Macro.t()
  @doc """
  Builds a `Matcha.Spec` that represents a destructuring, pattern matching, and re-structuring operation in a given `context`.

  The `context` may be #{Context.__core_context_aliases__() |> Keyword.keys() |> Enum.map_join(", ", &"`#{inspect(&1)}`")}, or a `Matcha.Context` module.
  This is detailed in the `Matcha.Context` docs.

  For more information on match specs, consult the `Matcha.Spec` docs.

  ## Examples

      iex> require Matcha
      ...> Matcha.spec(:table) do
      ...>   {x, y, x}
      ...>     when x > y and y > 0
      ...>       -> x
      ...>   {x, y, y}
      ...>     when x < y and y < 0
      ...>       -> y
      ...> end
      #Matcha.Spec<[{{:"$1", :"$2", :"$1"}, [{:andalso, {:>, :"$1", :"$2"}, {:>, :"$2", 0}}], [:"$1"]}, {{:"$1", :"$2", :"$2"}, [{:andalso, {:<, :"$1", :"$2"}, {:<, :"$2", 0}}], [:"$2"]}], context: Matcha.Context.Table>

  """
  defmacro spec(context, spec)

  defmacro spec(context, [do: clauses] = _spec) when is_list(clauses) do
    do_spec(__CALLER__, context, clauses)
  end

  defmacro spec(_context, _spec = [do: not_a_list]) when not is_list(not_a_list) do
    raise ArgumentError,
      message:
        "#{__MODULE__}.spec/2 must be provided with `->` clauses," <>
          " got: `#{Macro.to_string(not_a_list)}`"
  end

  defmacro spec(_context, not_a_block) do
    raise ArgumentError,
      message:
        "#{__MODULE__}.spec/2 requires a block argument," <>
          " got: `#{Macro.to_string(not_a_block)}`"
  end

  @spec spec(Macro.t()) :: Macro.t()
  @doc """
  Builds a `Matcha.Spec` that represents a destructuring, pattern matching, and re-structuring operation on in-memory data.

  Identical to calling `spec/2` with a `:filter_map` context. Note that this context is mostly used to experiment with match specs,
  and you should generally prefer calling `spec/2` with either a `:table` or `:trace` context
  depending on which `Matcha` APIs you intend to use:

  - Use the `:trace` context if you intend to query data with `Matcha.Trace` functions
  - Use the `:table` context if you intend to trace code execution with the `Matcha.Table` functions

  ## Examples

      iex> require Matcha
      ...> Matcha.spec do
      ...>   {x, y, x}
      ...>     when x > y and y > 0
      ...>       -> x
      ...>   {x, y, y}
      ...>     when x < y and y < 0
      ...>       -> y
      ...> end
      #Matcha.Spec<[{{:"$1", :"$2", :"$1"}, [{:andalso, {:>, :"$1", :"$2"}, {:>, :"$2", 0}}], [:"$1"]}, {{:"$1", :"$2", :"$2"}, [{:andalso, {:<, :"$1", :"$2"}, {:<, :"$2", 0}}], [:"$2"]}], context: Matcha.Context.FilterMap>

  """
  defmacro spec(spec)

  defmacro spec([do: clauses] = _spec) when is_list(clauses) do
    do_spec(__CALLER__, @default_context, clauses)
  end

  defmacro spec(_spec = [do: not_a_list]) when not is_list(not_a_list) do
    raise ArgumentError,
      message:
        "#{__MODULE__}.spec/1 must be provided with `->` clauses," <>
          " got: `#{Macro.to_string(not_a_list)}`"
  end

  defmacro spec(not_a_block) do
    raise ArgumentError,
      message:
        "#{__MODULE__}.spec/1 requires a block argument," <>
          " got: `#{Macro.to_string(not_a_block)}`"
  end

  @doc """
  Traces `function` calls to `module`, executing a `spec` on matching arguments.

  Tracing is a powerful feature of the BEAM VM, allowing for near zero-cost
  monitoring of what is happening in running systems.
  The functions in `Matcha.Trace` provide utilities for accessing this functionality.

  One of the most powerful forms of tracing uses match specifications:
  rather that just print information on when a certain function signature
  with some number of arguments is invoked, they let you:

  - dissect the arguments in question with pattern-matching and guards
  - take special actions in response (documented in `Matcha.Context.Trace`)

  This macro is a shortcut for constructing a `spec` with the `:trace` context via `Matcha.spec/2`,
  and tracing the specified `module` and `function` with it via `Matcha.Trace.calls/4`.

  For more information on tracing in general, consult the `Matcha.Trace` docs.

  ## Examples

      iex> require Matcha
      ...> Matcha.trace_calls(Enum, :join, limit: 3) do
      ...>   [_enumerable] -> message("using default joiner")
      ...>   [_enumerable, ""] -> message("using default joiner (but explicitly)")
      ...>   [_enumerable, _custom] -> message("using custom joiner")
      ...> end
      ...> Enum.join(1..3)
      # Prints a trace message with "using default joiner" appended
      "123"
      iex> Enum.join(1..3, "")
      # Prints a trace message with "using default joiner (but explicitly)" appended
      "123"
      iex> Enum.join(1..3, ", ")
      # Prints a trace message with "using custom joiner" appended
      "1, 2, 3"

  """
  defmacro trace_calls(module, function, opts \\ [], spec) do
    quote do
      require Matcha.Trace

      Trace.calls(
        unquote(module),
        unquote(function),
        Trace.spec(unquote(spec)),
        unquote(opts)
      )
    end
  end
end