Be precise about usefulness vs reachability

This commit is contained in:
Nadrieril 2023-11-18 04:17:50 +01:00
parent 4e376cc104
commit 9aafc0b815
3 changed files with 159 additions and 94 deletions

View file

@ -1,6 +1,6 @@
use super::deconstruct_pat::{Constructor, DeconstructedPat, WitnessPat};
use super::usefulness::{
compute_match_usefulness, MatchArm, MatchCheckCtxt, Reachability, UsefulnessReport,
compute_match_usefulness, MatchArm, MatchCheckCtxt, Usefulness, UsefulnessReport,
};
use crate::errors::*;
@ -749,18 +749,18 @@ fn report_arm_reachability<'p, 'tcx>(
);
};
use Reachability::*;
use Usefulness::*;
let mut catchall = None;
for (arm, is_useful) in report.arm_usefulness.iter() {
match is_useful {
Unreachable => report_unreachable_pattern(arm.pat.span(), arm.hir_id, catchall),
Reachable(unreachables) if unreachables.is_empty() => {}
// The arm is reachable, but contains unreachable subpatterns (from or-patterns).
Reachable(unreachables) => {
let mut unreachables = unreachables.clone();
Redundant => report_unreachable_pattern(arm.pat.span(), arm.hir_id, catchall),
Useful(redundant_spans) if redundant_spans.is_empty() => {}
// The arm is reachable, but contains redundant subpatterns (from or-patterns).
Useful(redundant_spans) => {
let mut redundant_spans = redundant_spans.clone();
// Emit lints in the order in which they occur in the file.
unreachables.sort_unstable();
for span in unreachables {
redundant_spans.sort_unstable();
for span in redundant_spans {
report_unreachable_pattern(span, arm.hir_id, None);
}
}

View file

@ -1339,7 +1339,8 @@ pub(crate) struct DeconstructedPat<'p, 'tcx> {
fields: Fields<'p, 'tcx>,
ty: Ty<'tcx>,
span: Span,
reachable: Cell<bool>,
/// Whether removing this arm would change the behavior of the match expression.
useful: Cell<bool>,
}
impl<'p, 'tcx> DeconstructedPat<'p, 'tcx> {
@ -1353,7 +1354,7 @@ impl<'p, 'tcx> DeconstructedPat<'p, 'tcx> {
ty: Ty<'tcx>,
span: Span,
) -> Self {
DeconstructedPat { ctor, fields, ty, span, reachable: Cell::new(false) }
DeconstructedPat { ctor, fields, ty, span, useful: Cell::new(false) }
}
/// Note: the input patterns must have been lowered through
@ -1634,38 +1635,38 @@ impl<'p, 'tcx> DeconstructedPat<'p, 'tcx> {
}
}
/// We keep track for each pattern if it was ever reachable during the analysis. This is used
/// with `unreachable_spans` to report unreachable subpatterns arising from or patterns.
pub(super) fn set_reachable(&self) {
self.reachable.set(true)
/// We keep track for each pattern if it was ever useful during the analysis. This is used
/// with `redundant_spans` to report redundant subpatterns arising from or patterns.
pub(super) fn set_useful(&self) {
self.useful.set(true)
}
pub(super) fn is_reachable(&self) -> bool {
if self.reachable.get() {
pub(super) fn is_useful(&self) -> bool {
if self.useful.get() {
true
} else if self.is_or_pat() && self.iter_fields().any(|f| f.is_reachable()) {
} else if self.is_or_pat() && self.iter_fields().any(|f| f.is_useful()) {
// We always expand or patterns in the matrix, so we will never see the actual
// or-pattern (the one with constructor `Or`) in the column. As such, it will not be
// marked as reachable itself, only its children will. We recover this information here.
self.set_reachable();
// marked as useful itself, only its children will. We recover this information here.
self.set_useful();
true
} else {
false
}
}
/// Report the spans of subpatterns that were not reachable, if any.
pub(super) fn unreachable_spans(&self) -> Vec<Span> {
/// Report the spans of subpatterns that were not useful, if any.
pub(super) fn redundant_spans(&self) -> Vec<Span> {
let mut spans = Vec::new();
self.collect_unreachable_spans(&mut spans);
self.collect_redundant_spans(&mut spans);
spans
}
fn collect_unreachable_spans(&self, spans: &mut Vec<Span>) {
// We don't look at subpatterns if we already reported the whole pattern as unreachable.
if !self.is_reachable() {
fn collect_redundant_spans(&self, spans: &mut Vec<Span>) {
// We don't look at subpatterns if we already reported the whole pattern as redundant.
if !self.is_useful() {
spans.push(self.span);
} else {
for p in self.iter_fields() {
p.collect_unreachable_spans(spans);
p.collect_redundant_spans(spans);
}
}
}

View file

@ -1,8 +1,8 @@
//! # Match exhaustiveness and reachability algorithm
//! # Match exhaustiveness and redundancy algorithm
//!
//! This file contains the logic for exhaustiveness and reachability checking for pattern-matching.
//! This file contains the logic for exhaustiveness and usefulness checking for pattern-matching.
//! Specifically, given a list of patterns in a match, we can tell whether:
//! (a) a given pattern is reachable (reachability)
//! (a) a given pattern is redundant
//! (b) the patterns cover every possible value for the type (exhaustiveness)
//!
//! The algorithm implemented here is inspired from the one described in [this
@ -19,15 +19,15 @@
//! The algorithm is given as input a list of patterns, one for each arm of a match, and computes
//! the following:
//! - a set of values that match none of the patterns (if any),
//! - for each subpattern (taking into account or-patterns), whether it would catch any value that
//! isn't caught by a pattern before it, i.e. whether it is reachable.
//! - for each subpattern (taking into account or-patterns), whether removing it would change
//! anything about how the match executes, i.e. whether it is useful/not redundant.
//!
//! To a first approximation, the algorithm works by exploring all possible values for the type
//! being matched on, and determining which arm(s) catch which value. To make this tractable we
//! cleverly group together values, as we'll see below.
//!
//! The entrypoint of this file is the [`compute_match_usefulness`] function, which computes
//! reachability for each subpattern and exhaustiveness for the whole match.
//! usefulness for each subpattern and exhaustiveness for the whole match.
//!
//! In this page we explain the necessary concepts to understand how the algorithm works.
//!
@ -39,17 +39,17 @@
//! none of the `p_i`. We write `usefulness(p_1 .. p_n, q)` for a function that returns a list of
//! such values. The aim of this file is to compute it efficiently.
//!
//! This is enough to compute reachability: a pattern in a `match` expression is reachable iff it is
//! useful w.r.t. the patterns above it:
//! This is enough to compute usefulness: a pattern in a `match` expression is redundant iff it is
//! not useful w.r.t. the patterns above it:
//! ```compile_fail,E0004
//! # #![feature(exclusive_range_pattern)]
//! # fn foo() {
//! match Some(0u32) {
//! Some(0..100) => {},
//! Some(90..190) => {}, // reachable: `Some(150)` is matched by this but not the branch above
//! Some(50..150) => {}, // unreachable: all the values this matches are already matched by
//! Some(90..190) => {}, // useful: `Some(150)` is matched by this but not the branch above
//! Some(50..150) => {}, // redundant: all the values this matches are already matched by
//! // the branches above
//! None => {}, // reachable: `None` is matched by this but not the branches above
//! None => {}, // useful: `None` is matched by this but not the branches above
//! }
//! # }
//! ```
@ -246,18 +246,17 @@
//!
//!
//!
//! # Computing reachability and exhaustiveness in one go
//! # Computing usefulness and exhaustiveness in one go
//!
//! The algorithm we have described so far computes usefulness of each pattern in turn to check if
//! it is reachable, and ends by checking if `_` is useful to determine exhaustiveness of the whole
//! match. In practice, instead of doing "for each pattern { for each constructor { ... } }", we do
//! "for each constructor { for each pattern { ... } }". This allows us to compute everything in one
//! go.
//! The algorithm we have described so far computes usefulness of each pattern in turn, and ends by
//! checking if `_` is useful to determine exhaustiveness of the whole match. In practice, instead
//! of doing "for each pattern { for each constructor { ... } }", we do "for each constructor { for
//! each pattern { ... } }". This allows us to compute everything in one go.
//!
//! [`Matrix`] stores the set of pattern-tuples under consideration. We track reachability of each
//! [`Matrix`] stores the set of pattern-tuples under consideration. We track usefulness of each
//! row mutably in the matrix as we go along. We ignore witnesses of usefulness of the match rows.
//! We gather witnesses of the usefulness of `_` in [`WitnessMatrix`]. The algorithm that computes
//! all this is in [`compute_exhaustiveness_and_reachability`].
//! all this is in [`compute_exhaustiveness_and_usefulness`].
//!
//! See the full example at the bottom of this documentation.
//!
@ -279,7 +278,7 @@
//! ```
//!
//! In this example, trying any of `0`, `1`, .., `49` will give the same specialized matrix, and
//! thus the same reachability/exhaustiveness results. We can thus accelerate the algorithm by
//! thus the same usefulness/exhaustiveness results. We can thus accelerate the algorithm by
//! trying them all at once. Here in fact, the only cases we need to consider are: `0..50`,
//! `50..=100`, `101..=150`,`151..=200` and `201..`.
//!
@ -299,15 +298,16 @@
//! This is done in [`ConstructorSet::split`] and explained in [`super::deconstruct_pat`].
//!
//!
//!
//! # Or-patterns
//!
//! What we have described so far works well if there are no or-patterns. To handle them, if the
//! first pattern of a row in the matrix is an or-pattern, we expand it by duplicating the rest of
//! the row as necessary. This is handled automatically in [`Matrix`].
//!
//! This makes reachability tracking subtle, because we also want to compute whether an alternative
//! of an or-pattern is unreachable, e.g. in `Some(_) | Some(0)`. We track reachability of each
//! subpattern by interior mutability in [`DeconstructedPat`] with `set_reachable`/`is_reachable`.
//! This makes usefulness tracking subtle, because we also want to compute whether an alternative
//! of an or-pattern is redundant, e.g. in `Some(_) | Some(0)`. We track usefulness of each
//! subpattern by interior mutability in [`DeconstructedPat`] with `set_useful`/`is_useful`.
//!
//! It's unfortunate that we have to use interior mutability, but believe me (Nadrieril), I have
//! tried [other](https://github.com/rust-lang/rust/pull/80104)
@ -332,6 +332,69 @@
//!
//!
//!
//! # Usefulness vs reachability, validity, and empty patterns
//!
//! This is likely the subtlest aspect of the algorithm. To be fully precise, a match doesn't
//! operate on a value, it operates on a place. In certain unsafe circumstances, it is possible for
//! a place to not contain valid data for its type. This has subtle consequences for empty types.
//! Take the following:
//!
//! ```rust
//! enum Void {}
//! let x: u8 = 0;
//! let ptr: *const Void = &x as *const u8 as *const Void;
//! unsafe {
//! match *ptr {
//! _ => println!("Reachable!"),
//! }
//! }
//! ```
//!
//! In this example, `ptr` is a valid pointer pointing to a place with invalid data. The `_` pattern
//! does not look at the contents of `*ptr`, so this is ok and the arm is taken. In other words,
//! despite the place we are inspecting being of type `Void`, there is a reachable arm. If the
//! arm had a binding however:
//!
//! ```rust
//! # #[derive(Copy, Clone)]
//! # enum Void {}
//! # let x: u8 = 0;
//! # let ptr: *const Void = &x as *const u8 as *const Void;
//! # unsafe {
//! match *ptr {
//! _a => println!("Unreachable!"),
//! }
//! # }
//! ```
//!
//! Here the binding loads the value of type `Void` from the `*ptr` place. In this example, this
//! causes UB since the data is not valid. In the general case, this asserts validity of the data at
//! `*ptr`. Either way, this arm will never be taken.
//!
//! Finally, let's consider the empty match `match *ptr {}`. If we consider this exhaustive, then
//! having invalid data at `*ptr` is invalid. In other words, the empty match is semantically
//! equivalent to the `_a => ...` match. In the interest of explicitness, we prefer the case with an
//! arm, hence we won't tell the user to remove the `_a` arm. In other words, the `_a` arm is
//! unreachable yet not redundant. This is why we lint on redundant arms rather than unreachable
//! arms, despite the fact that the lint says "unreachable".
//!
//! These considerations only affects certain places, namely those that can contain non-valid data
//! without UB. These are: pointer dereferences, reference dereferences, and union field accesses.
//! We track in the algorithm whether a given place is known to contain valid data. This is done
//! first by inspecting the scrutinee syntactically (which gives us `cx.known_valid_scrutinee`), and
//! then by tracking validity of each column of the matrix (which correspond to places) as we
//! recurse into subpatterns. That second part is done through [`ValidityConstraint`], most notably
//! [`ValidityConstraint::specialize`].
//!
//! Having said all that, in practice we don't fully follow what's been presented in this section.
//! Under `exhaustive_patterns`, we allow omitting empty arms even in `!known_valid` places, for
//! backwards-compatibility until we have a better alternative. Without `exhaustive_patterns`, we
//! mostly treat empty types as inhabited, except specifically a non-nested `!` or empty enum. In
//! this specific case we also allow the empty match regardless of place validity, for
//! backwards-compatibility. Hopefully we can eventually deprecate this.
//!
//!
//!
//! # Full example
//!
//! We illustrate a full run of the algorithm on the following match.
@ -348,7 +411,7 @@
//! ```
//!
//! We keep track of the original row for illustration purposes, this is not what the algorithm
//! actually does (it tracks reachability as a boolean on each row).
//! actually does (it tracks usefulness as a boolean on each row).
//!
//! ```text
//! ┐ Patterns:
@ -377,7 +440,7 @@
//! │ │ │ ├─┐ Patterns:
//! │ │ │ │ │ 1. `[]`
//! │ │ │ │ │
//! │ │ │ │ │ We note arm 1 is reachable (by `Pair(Some(0), true)`).
//! │ │ │ │ │ We note arm 1 is useful (by `Pair(Some(0), true)`).
//! │ │ │ ├─┘
//! │ │ │ │
//! │ │ │ │ Specialize with `false`:
@ -385,7 +448,7 @@
//! │ │ │ │ │ 1. `[]`
//! │ │ │ │ │ 3. `[]`
//! │ │ │ │ │
//! │ │ │ │ │ We note arm 1 is reachable (by `Pair(Some(0), false)`).
//! │ │ │ │ │ We note arm 1 is useful (by `Pair(Some(0), false)`).
//! │ │ │ ├─┘
//! │ │ ├─┘
//! │ │ │
@ -408,7 +471,7 @@
//! │ │ │ ├─┐ Patterns:
//! │ │ │ │ │ 2. `[]`
//! │ │ │ │ │
//! │ │ │ │ │ We note arm 2 is reachable (by `Pair(Some(1..), false)`).
//! │ │ │ │ │ We note arm 2 is useful (by `Pair(Some(1..), false)`).
//! │ │ │ ├─┘
//! │ │ │ │
//! │ │ │ │ Total witnesses for `1..`:
@ -442,7 +505,7 @@
//! │ │ ├─┐ Patterns:
//! │ │ │ │ 2. `[]`
//! │ │ │ │
//! │ │ │ │ We note arm 2 is reachable (by `Pair(None, false)`).
//! │ │ │ │ We note arm 2 is useful (by `Pair(None, false)`).
//! │ │ ├─┘
//! │ │ │
//! │ │ │ Total witnesses for `None`:
@ -466,7 +529,7 @@
//! ```
//!
//! We conclude:
//! - Arm 3 is unreachable (it was never marked as reachable);
//! - Arm 3 is redundant (it was never marked as useful);
//! - The match is not exhaustive;
//! - Adding arms with `Pair(Some(1..), true)` and `Pair(None, true)` would make the match exhaustive.
//!
@ -639,13 +702,13 @@ struct MatrixRow<'p, 'tcx> {
/// Whether the original arm had a guard. This is inherited when specializing.
is_under_guard: bool,
/// When we specialize, we remember which row of the original matrix produced a given row of the
/// specialized matrix. When we unspecialize, we use this to propagate reachability back up the
/// specialized matrix. When we unspecialize, we use this to propagate usefulness back up the
/// callstack.
parent_row: usize,
/// False when the matrix is just built. This is set to `true` by
/// [`compute_exhaustiveness_and_reachability`] if the arm is found to be reachable.
/// [`compute_exhaustiveness_and_usefulness`] if the arm is found to be useful.
/// This is reset to `false` when specializing.
reachable: bool,
useful: bool,
}
impl<'p, 'tcx> MatrixRow<'p, 'tcx> {
@ -672,7 +735,7 @@ impl<'p, 'tcx> MatrixRow<'p, 'tcx> {
pats: patstack,
parent_row: self.parent_row,
is_under_guard: self.is_under_guard,
reachable: false,
useful: false,
})
}
@ -688,7 +751,7 @@ impl<'p, 'tcx> MatrixRow<'p, 'tcx> {
pats: self.pats.pop_head_constructor(pcx, ctor),
parent_row,
is_under_guard: self.is_under_guard,
reachable: false,
useful: false,
}
}
}
@ -741,7 +804,7 @@ impl<'p, 'tcx> Matrix<'p, 'tcx> {
pats: PatStack::from_pattern(arm.pat),
parent_row: row_id, // dummy, we won't read it
is_under_guard: arm.has_guard,
reachable: false,
useful: false,
};
matrix.expand_and_push(v);
}
@ -940,7 +1003,7 @@ impl<'tcx> WitnessStack<'tcx> {
/// Represents a set of pattern-tuples that are witnesses of non-exhaustiveness for error
/// reporting. This has similar invariants as `Matrix` does.
///
/// The `WitnessMatrix` returned by [`compute_exhaustiveness_and_reachability`] obeys the invariant
/// The `WitnessMatrix` returned by [`compute_exhaustiveness_and_usefulness`] obeys the invariant
/// that the union of the input `Matrix` and the output `WitnessMatrix` together matches the type
/// exhaustively.
///
@ -1029,7 +1092,7 @@ impl<'tcx> WitnessMatrix<'tcx> {
/// The core of the algorithm.
///
/// This recursively computes witnesses of the non-exhaustiveness of `matrix` (if any). Also tracks
/// usefulness of each row in the matrix (in `row.reachable`). We track reachability of each
/// usefulness of each row in the matrix (in `row.useful`). We track usefulness of each
/// subpattern using interior mutability in `DeconstructedPat`.
///
/// The input `Matrix` and the output `WitnessMatrix` together match the type exhaustively.
@ -1038,10 +1101,10 @@ impl<'tcx> WitnessMatrix<'tcx> {
/// - specialization, where we dig into the rows that have a specific constructor and call ourselves
/// recursively;
/// - unspecialization, where we lift the results from the previous step into results for this step
/// (using `apply_constructor` and by updating `row.reachable` for each parent row).
/// (using `apply_constructor` and by updating `row.useful` for each parent row).
/// This is all explained at the top of the file.
#[instrument(level = "debug", skip(cx, is_top_level), ret)]
fn compute_exhaustiveness_and_reachability<'p, 'tcx>(
fn compute_exhaustiveness_and_usefulness<'p, 'tcx>(
cx: &MatchCheckCtxt<'p, 'tcx>,
matrix: &mut Matrix<'p, 'tcx>,
is_top_level: bool,
@ -1050,10 +1113,10 @@ fn compute_exhaustiveness_and_reachability<'p, 'tcx>(
let Some(ty) = matrix.head_ty() else {
// The base case: there are no columns in the matrix. We are morally pattern-matching on ().
// A row is reachable iff it has no (unguarded) rows above it.
// A row is useful iff it has no (unguarded) rows above it.
for row in matrix.rows_mut() {
// All rows are reachable until we find one without a guard.
row.reachable = true;
// All rows are useful until they're not.
row.useful = true;
if !row.is_under_guard {
// There's an unguarded row, so the match is exhaustive, and any subsequent row is
// unreachable.
@ -1095,7 +1158,7 @@ fn compute_exhaustiveness_and_reachability<'p, 'tcx>(
// Dig into rows that match `ctor`.
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor);
let mut witnesses = ensure_sufficient_stack(|| {
compute_exhaustiveness_and_reachability(cx, &mut spec_matrix, false)
compute_exhaustiveness_and_usefulness(cx, &mut spec_matrix, false)
});
if !only_report_missing || matches!(ctor, Constructor::Missing) {
@ -1113,14 +1176,14 @@ fn compute_exhaustiveness_and_reachability<'p, 'tcx>(
// A parent row is useful if any of its children is.
for child_row in spec_matrix.rows() {
let parent_row = &mut matrix.rows[child_row.parent_row];
parent_row.reachable = parent_row.reachable || child_row.reachable;
parent_row.useful = parent_row.useful || child_row.useful;
}
}
// Record that the subpattern is reachable.
// Record usefulness in the patterns.
for row in matrix.rows() {
if row.reachable {
row.head().set_reachable();
if row.useful {
row.head().set_useful();
}
}
@ -1130,8 +1193,8 @@ fn compute_exhaustiveness_and_reachability<'p, 'tcx>(
/// A column of patterns in the matrix, where a column is the intuitive notion of "subpatterns that
/// inspect the same subvalue/place".
/// This is used to traverse patterns column-by-column for lints. Despite similarities with
/// [`compute_exhaustiveness_and_reachability`], this does a different traversal. Notably this is
/// linear in the depth of patterns, whereas `compute_exhaustiveness_and_reachability` is worst-case
/// [`compute_exhaustiveness_and_usefulness`], this does a different traversal. Notably this is
/// linear in the depth of patterns, whereas `compute_exhaustiveness_and_usefulness` is worst-case
/// exponential (exhaustiveness is NP-complete). The core difference is that we treat sub-columns
/// separately.
///
@ -1351,28 +1414,29 @@ pub(crate) struct MatchArm<'p, 'tcx> {
pub(crate) has_guard: bool,
}
/// Indicates whether or not a given arm is reachable.
/// Indicates whether or not a given arm is useful.
#[derive(Clone, Debug)]
pub(crate) enum Reachability {
/// The arm is reachable. This additionally carries a set of or-pattern branches that have been
/// found to be unreachable despite the overall arm being reachable. Used only in the presence
/// of or-patterns, otherwise it stays empty.
Reachable(Vec<Span>),
/// The arm is unreachable.
Unreachable,
pub(crate) enum Usefulness {
/// The arm is useful. This additionally carries a set of or-pattern branches that have been
/// found to be redundant despite the overall arm being useful. Used only in the presence of
/// or-patterns, otherwise it stays empty.
Useful(Vec<Span>),
/// The arm is redundant and can be removed without changing the behavior of the match
/// expression.
Redundant,
}
/// The output of checking a match for exhaustiveness and arm reachability.
/// The output of checking a match for exhaustiveness and arm usefulness.
pub(crate) struct UsefulnessReport<'p, 'tcx> {
/// For each arm of the input, whether that arm is reachable after the arms above it.
pub(crate) arm_usefulness: Vec<(MatchArm<'p, 'tcx>, Reachability)>,
/// For each arm of the input, whether that arm is useful after the arms above it.
pub(crate) arm_usefulness: Vec<(MatchArm<'p, 'tcx>, Usefulness)>,
/// If the match is exhaustive, this is empty. If not, this contains witnesses for the lack of
/// exhaustiveness.
pub(crate) non_exhaustiveness_witnesses: Vec<WitnessPat<'tcx>>,
}
/// The entrypoint for this file. Computes whether a match is exhaustive and which of its arms are
/// reachable.
/// useful.
#[instrument(skip(cx, arms), level = "debug")]
pub(crate) fn compute_match_usefulness<'p, 'tcx>(
cx: &MatchCheckCtxt<'p, 'tcx>,
@ -1380,8 +1444,7 @@ pub(crate) fn compute_match_usefulness<'p, 'tcx>(
scrut_ty: Ty<'tcx>,
) -> UsefulnessReport<'p, 'tcx> {
let mut matrix = Matrix::new(cx, arms, scrut_ty);
let non_exhaustiveness_witnesses =
compute_exhaustiveness_and_reachability(cx, &mut matrix, true);
let non_exhaustiveness_witnesses = compute_exhaustiveness_and_usefulness(cx, &mut matrix, true);
let non_exhaustiveness_witnesses: Vec<_> = non_exhaustiveness_witnesses.single_column();
let arm_usefulness: Vec<_> = arms
@ -1389,12 +1452,13 @@ pub(crate) fn compute_match_usefulness<'p, 'tcx>(
.copied()
.map(|arm| {
debug!(?arm);
let reachability = if arm.pat.is_reachable() {
Reachability::Reachable(arm.pat.unreachable_spans())
// We warn when a pattern is not useful.
let usefulness = if arm.pat.is_useful() {
Usefulness::Useful(arm.pat.redundant_spans())
} else {
Reachability::Unreachable
Usefulness::Redundant
};
(arm, reachability)
(arm, usefulness)
})
.collect();
let report = UsefulnessReport { arm_usefulness, non_exhaustiveness_witnesses };