Skip to main content

lib/rustler/match_spec.ex

defmodule Rustler.MatchSpec do
  @moduledoc """
  Small, Erlang-inspired match specifications for Rustler event streams.

  `Rustler.MatchSpec` provides an Elixir macro that turns a restricted, idiomatic
  pattern-matching syntax into data shaped like Erlang match specifications:

      [{match_head, match_guards, match_body}]

  Parser/NIF packages can use this data to select and project native events
  without serializing whole native ASTs to Elixir.

  ## Example

      import Rustler.MatchSpec

      spec =
        match_spec do
          {:css_url, url, start, finish} when is_binary(url) ->
            %{url: url, start: start, end: finish}
        end

      spec == [
        {{:css_url, :"$1", :"$2", :"$3"}, [{:is_binary, :"$1"}], [
          %{url: :"$1", start: :"$2", end: :"$3"}
        ]}
      ]

  This package intentionally does not define parser-specific event names. OXC,
  Vize, or an HTML parser should define their own event constructors and return
  structs while reusing the same match-spec shape.
  """

  alias Rustler.MatchSpec.Compiler

  @variables Map.new(1..255, &{&1, :"$#{&1}"})

  @typedoc "A match variable atom such as `:'$1'`."
  @type variable :: atom()

  @typedoc "A compiled match specification term."
  @type t :: [{term(), [term()], [term()]}]

  @typedoc "An opaque native selector resource returned by `compile/1`."
  @opaque selector :: reference()

  @doc """
  Compile restricted Elixir pattern syntax into a match-spec term.

  Supported clause form:

      pattern [when guard] -> body

  Variables first seen in the pattern are assigned `:"$1"`, `:"$2"`, etc.
  The same variables can be referenced in guards and body terms.
  """
  defmacro match_spec(do: block) do
    block
    |> Compiler.compile_block(__CALLER__)
    |> Macro.escape()
  end

  @doc """
  Compile a match specification into an opaque native selector resource.

  Reuse the returned selector when applying the same spec repeatedly.
  """
  @spec compile(t()) :: selector()
  def compile(spec) when is_list(spec), do: __MODULE__.Native.compile(spec)

  @doc """
  Select projected results from a list of BEAM term events.

  The second argument can be either a raw match spec or a selector returned by
  `compile/1`. This is primarily the reference/test harness for the native
  evaluator. Parser packages should call the Rust crate directly from their own
  NIFs so native AST traversal and event projection stay in one native call.
  """
  @spec select([term()], t() | selector()) :: [term()]
  def select(events, spec_or_selector) when is_list(events),
    do: __MODULE__.Native.select(events, spec_or_selector)

  @doc "Returns a match variable atom, e.g. `var(1) == :\"$1\"`."
  @spec var(pos_integer()) :: variable()
  def var(index) when is_integer(index) and index in 1..255, do: @variables[index]
end