Rustler.MatchSpec
Reusable Erlang-style match specifications for Rustler native event streams.
Rustler.MatchSpec provides a small Elixir DSL and a Rust evaluator for selecting
and projecting compact native events without exposing large native data
structures to the BEAM.
This is useful for Rustler NIFs that produce many small structured facts from native code, such as parser events, CSS dependencies, trace events, or resource metadata. Instead of returning a whole native data structure to Elixir and walking it on the BEAM, the NIF can expose a stream of tuple-like events and let Elixir select the facts it wants.
The Elixir side builds a match spec. The Rust side decodes it into a selector and applies it to native events.
Why
A common Rustler pattern is:
- parse or analyze something in Rust,
- serialize a large AST or result tree to Elixir,
- walk that structure in Elixir to find a few pieces of information.
rustler_match_spec supports a narrower shape:
- parse or analyze something in Rust,
- emit compact native events,
- filter/project those events with an Elixir-provided selector,
- return only the selected results.
This is useful when the parser is already native and Elixir only needs facts such as import specifiers, asset URLs, source ranges, or dependency records.
Elixir usage
import Rustler.MatchSpec
spec =
match_spec do
{:event, kind, value} when kind in [:import, :url] ->
{kind, value}
end
Rustler.MatchSpec.select([
{:event, :import, "./app.js"},
{:event, :env, "MODE"},
{:event, :url, "./font.woff2"}
], spec)
#=> [{:import, "./app.js"}, {:url, "./font.woff2"}]
For repeated use, compile once:
selector = Rustler.MatchSpec.compile(spec)
Rustler.MatchSpec.select(events, selector)
The macro compiles to plain match-spec-shaped data:
[{match_head, match_guards, match_body}]
Selector terms can also be passed into your own NIF APIs.
Rust integration
Rust NIF crates can implement MatchEvent for their own event types and reuse
the same selector evaluator. This lets a NIF match tuple-shaped patterns such as
{:import, source, start, finish} without allocating whole intermediate event
tuples.
use rustler::{Atom, Env, NifResult, Term};
use rustler_match_spec::{MatchEvent, Selector, ValueRef};
struct ImportEvent<'a> {
import_atom: Atom,
source: &'a str,
start: u32,
end: u32,
}
impl<'a> MatchEvent<'a> for ImportEvent<'a> {
fn tag(&self) -> Atom {
self.import_atom
}
fn arity(&self) -> usize {
4
}
fn positional_field(&self, index: usize) -> Option<ValueRef<'a>> {
match index {
1 => Some(ValueRef::Str(self.source)),
2 => Some(ValueRef::U64(self.start as u64)),
3 => Some(ValueRef::U64(self.end as u64)),
_ => None,
}
}
fn field(&self, _name: Atom) -> Option<ValueRef<'a>> {
None
}
}
fn select_imports<'env>(env: Env<'env>, selector: Selector) -> NifResult<Vec<Term<'env>>> {
let mut out = Vec::new();
for event in native_import_events() {
selector.run_event(env, &event, &mut out)?;
}
Ok(out)
}
# fn native_import_events<'a>() -> Vec<ImportEvent<'a>> { vec![] }
Tuple patterns match tag() plus positional fields. For a pattern like:
{:import, source, start, finish}
tag() is matched against :import, and positional_field(1),
positional_field(2), and positional_field(3) provide the remaining tuple
elements.
API surface
The main Elixir functions are:
Rustler.MatchSpec.match_spec/1Rustler.MatchSpec.compile/1Rustler.MatchSpec.select/2
The main Rust types are:
Selector— decoded/compiled match-spec plan.MatchEvent— trait implemented by native event types.ValueRef— borrowed value returned by event fields.
Real use
The crate was originally extracted for the Elixir Volt toolchain. OXC and Vize use it to implement parser-backed selectors for JavaScript and CSS facts. Volt then uses those selectors to avoid repeated parse + AST-walk passes for imports, assets, workers, glob imports, and environment references.
On a Monaco Editor build in Exograph, this helped reduce OXC parse calls from 8043 to 3236 and improved the warm build from about 17.5s to about 14.3s.
Development
mix deps.get
mix ci
License
MIT