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