Pruning Irrelevant Pattern Columns: How rustc's Match Checker Went From Exponential to Linear
rustc's exhaustiveness checker uses Maranget's usefulness algorithm, plus a relevancy rule that skips columns matching strictly fewer rows - turning an exponential blowup into a linear one.
TL;DR: rustc’s match exhaustiveness checker implements Maranget’s usefulness algorithm, specialised for Rust’s patterns. The interesting part is an extra relevancy rule: when a column contains a wildcard and some constructors are missing, specializing with those missing constructors subsumes the others — so the checker can skip whole subtrees. In one pathological case that turned exponential work into linear work.
The algorithm
// compiler/rustc_pattern_analysis/src/usefulness.rs:8-11, 110-135
//! The algorithm implemented here is inspired from the one described in [this
//! paper](http://moscova.inria.fr/~maranget/papers/warn/index.html). We have however changed it in
//! various ways to accommodate the variety of patterns that Rust supports. We thus explain our
//! version here, without being as precise.
...
//! # Specialization
//!
//! The examples in the previous section motivate the operation at the heart of the algorithm:
//! "specialization". It captures this idea of "removing one layer of constructor".
//!
//! `specialize(c, p)` takes a value-only constructor `c` and a pattern `p`, and returns a
//! pattern-tuple or nothing. It works as follows:
//!
//! - Specializing for the wrong constructor returns nothing
//!
//! - `specialize(None, Some(p0)) := <nothing>`
//! - `specialize([,,,], [p0]) := <nothing>`
//!
//! - Specializing for the correct constructor returns a tuple of the fields
//!
//! - `specialize(Variant1, Variant1(p0, p1, p2)) := (p0, p1, p2)`
//! - `specialize(Foo{ bar, baz, quz }, Foo { bar: p0, baz: p1, .. }) := (p0, p1, _)`
//! - `specialize([,,,], [p0, .., p1]) := (p0, _, _, p1)`
The core operation is specialization: peel one constructor off the head of a pattern, replacing it with its sub-fields. A matrix of patterns is specialized column by column; a pattern is useful if it can match something the other patterns cannot; and the whole matrix is exhaustive if a virtual wildcard row is not useful.
The blowup
// compiler/rustc_pattern_analysis/src/usefulness.rs:302-361
//! Relevancy allowed us to skip the case `(true, true, _)` entirely. In some cases this pruning can
//! give drastic speedups. The case this was built for is the following (#118437):
//!
//! ```ignore(illustrative)
//! match foo {
//! (true, _, _, _, ..) => 1,
//! (_, true, _, _, ..) => 2,
//! (_, _, true, _, ..) => 3,
//! (_, _, _, true, ..) => 4,
//! ...
//! }
//! ```
//!
//! Without considering relevancy, we would explore all 2^n combinations of the `true` and `Missing`
//! constructors. Relevancy tells us that e.g. `(true, true, false, false, false, ...)` is
//! irrelevant for all the rows. This allows us to skip all cases with more than one `true`
//! constructor, changing the runtime from exponential to linear.
That is the whole story of the optimisation, told in the source. A match
with n boolean-ish columns where each row fixes exactly one column would
require exploring all 2ⁿ combinations of constructors.
The implementation
Each row of the matrix carries a relevancy flag:
// compiler/rustc_pattern_analysis/src/usefulness.rs:1018-1025
struct PatStack<'p, Cx: PatCx> {
// Rows of len 1 are very common, which is why `SmallVec[_; 2]` works well.
pats: SmallVec<[PatOrWild<'p, Cx>; 2]>,
/// Sometimes we know that as far as this row is concerned, the current case is already handled
/// by a different, more general, case. When the case is irrelevant for all rows this allows us
/// to skip a case entirely. This is purely an optimization. See at the top for details.
relevant: bool,
}The flag is propagated during specialization:
// compiler/rustc_pattern_analysis/src/usefulness.rs:1081-1085
// `ctor` is relevant for this row if it is the actual constructor of this row, or if the
// row has a wildcard and `ctor` is relevant for wildcards.
let ctor_is_relevant =
!matches!(self.head().ctor(), Constructor::Wildcard) || ctor_is_relevant;
Ok(PatStack { pats: new_pats, relevant: self.relevant && ctor_is_relevant })and the subtree is abandoned as soon as no row is relevant:
// compiler/rustc_pattern_analysis/src/usefulness.rs:1704-1709
if !matrix.wildcard_row_is_relevant && matrix.rows().all(|r| !r.pats.relevant) {
// Here we know that nothing will contribute further to exhaustiveness or usefulness. This
// is purely an optimization: skipping this check doesn't affect correctness. See the top of the
// file for details.
return Ok(WitnessMatrix::empty());
}The rule that decides relevancy per constructor:
// compiler/rustc_pattern_analysis/src/usefulness.rs:1739-1748
for ctor in split_ctors {
// Dig into rows that match `ctor`.
debug!("specialize({:?})", ctor);
// `ctor` is *irrelevant* if there's another constructor in `split_ctors` that matches
// strictly fewer rows. In that case we can sometimes skip it. See the top of the file for
// details.
let ctor_is_relevant = matches!(ctor, Constructor::Missing)
|| missing_ctors.is_empty()
|| mcx.tycx.exhaustive_witnesses();The intuition: if a column contains a wildcard row and some constructors
are missing from that column, then the missing constructor already matches
“everything the wildcard matches, plus more”. Specializing with any other
constructor therefore yields no more information than specializing with
Missing, so it can be skipped.
Constructor splitting
The other half of the algorithm is choosing which constructors to try. That is the “split” step, and it has a precise contract:
// compiler/rustc_pattern_analysis/src/constructor.rs:1011-1037
/// Describes the result of analyzing the constructors in a column of a match.
///
/// `present` is morally the set of constructors present in the column, and `missing` is the set of
/// constructors that exist in the type but are not present in the column.
///
/// More formally, if we discard wildcards from the column, this respects the following constraints:
/// 1. the union of `present`, `missing` and `missing_empty` covers all the constructors of the type
/// 2. each constructor in `present` is covered by something in the column
/// 3. no constructor in `missing` or `missing_empty` is covered by anything in the column
/// 4. each constructor in the column is equal to the union of one or more constructors in `present`
/// 5. `missing` does not contain empty constructors (see discussion about emptiness at the top of
/// the file);
/// 6. `missing_empty` contains only empty constructors
/// 7. constructors in `present`, `missing` and `missing_empty` are split for the column; in other
/// words, they are either fully included in or fully disjoint from each constructor in the
/// column. In yet other words, there are no non-trivial intersections like between `0..10` and
/// `5..15`.
Point 7 is the subtle one: the splitting has to make overlapping
constructors disjoint before iterating, which is why overlapping integer
ranges like 0..10 and 5..15 need special treatment — the split is
computed so that every pair is either fully included or fully disjoint.
The overall shape:
// compiler/rustc_pattern_analysis/src/constructor.rs:36-49
//! Constructor splitting looks at the constructors in the first column of the matrix and constructs
//! such a sensible set of constructors. Formally, we want to find a smallest disjoint set of
//! constructors:
//! - Whose union covers the whole type, and
//! - That have no non-trivial intersection with any of the constructors in the column (i.e. they're
//! each either disjoint with or covered by any given column constructor).
//!
//! We compute this in two steps: first [`PatCx::ctors_for_ty`] determines the
//! set of all possible constructors for the type. Then [`ConstructorSet::split`] looks at the
//! column of constructors and splits the set into groups accordingly. The precise invariants of
//! [`ConstructorSet::split`] is described in [`SplitConstructorSet`].
Witness reconstruction
The algorithm does not just decide whether a match is exhaustive — it produces the witness, the value that was not matched, which becomes the “missing pattern” in the error message. That is done by “unspecializing”: the witnesses computed on specialized matrices are pushed back through the constructors:
// compiler/rustc_pattern_analysis/src/usefulness.rs:1548-1579
fn apply_constructor(
&mut self,
pcx: &PlaceCtxt<'_, Cx>,
missing_ctors: &[Constructor<Cx>],
ctor: &Constructor<Cx>,
) {
...
if matches!(ctor, Constructor::Missing) {
// We got the special `Missing` constructor that stands for the constructors not present
// in the match. For each missing constructor `c`, we add a `c(_, _, _)` witness
// appropriately filled with wildcards.
let mut ret = Self::empty();
for ctor in missing_ctors {
let pat = pcx.wild_from_ctor(ctor.clone());
// Clone `self` and add `c(_, _, _)` to each of its witnesses.
let mut wit_matrix = self.clone();
wit_matrix.push_pattern(pat);
ret.extend(wit_matrix);
}
*self = ret;
} else {
// Any other constructor we unspecialize as expected.
for witness in std::mem::take(&mut self.0) {
self.0.extend(witness.apply_constructor(pcx, ctor));
}
}
}The parent_row back-pointer in each specialized row makes
unspecialization O(rows) with no search:
// compiler/rustc_pattern_analysis/src/usefulness.rs:1317-1334
fn unspecialize(&mut self, specialized: Self) {
for child_row in specialized.rows() {
let parent_row_id = child_row.parent_row;
let parent_row = &mut self.rows[parent_row_id];
// A parent row is useful if any of its children is.
parent_row.useful |= child_row.useful;
...Where it lives
compiler/rustc_pattern_analysis/src/usefulness.rs— the algorithm, the relevancy rule,WitnessMatrix.compiler/rustc_pattern_analysis/src/constructor.rs—ConstructorSet::splitand its seven invariants.compiler/rustc_pattern_analysis/src/pat.rs— the pattern model.
Takeaway
Maranget’s algorithm is elegant but has a known worst case. The fix here is not a better data structure — it is an extra relevance rule that says “this branch of the search cannot produce new information, skip it”. The rule is expressed as a single boolean per matrix row, propagated through the recursion, and checked once per node. The cost is one flag; the saving is exponential.