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