Skip to content

Commit 1a086e4

Browse files
committed
Auto merge of #118796 - Nadrieril:fix-exponential-id-match-2, r=cjgillot
Exhaustiveness: Improve complexity on some wide matches #118437 revealed an exponential case in exhaustiveness checking. While [exponential cases are unavoidable](https://compilercrim.es/rust-np/), this one only showed up after my #117611 rewrite of the algorithm. I remember anticipating a case like this and dismissing it as unrealistic, but here we are :'). The tricky match is as follows: ```rust match command { BaseCommand { field01: true, .. } => {} BaseCommand { field02: true, .. } => {} BaseCommand { field03: true, .. } => {} BaseCommand { field04: true, .. } => {} BaseCommand { field05: true, .. } => {} BaseCommand { field06: true, .. } => {} BaseCommand { field07: true, .. } => {} BaseCommand { field08: true, .. } => {} BaseCommand { field09: true, .. } => {} BaseCommand { field10: true, .. } => {} // ...20 more of the same _ => {} } ``` To fix this, this PR formalizes a concept of "relevancy" (naming is hard) that was already used to decide what patterns to report. Now we track it for every row, which in wide matches like the above can drastically cut on the number of cases we explore. After this fix, the above match is checked with linear-many cases instead of exponentially-many. Fixes #118437 r? `@cjgillot`
2 parents ebb821f + efb04e6 commit 1a086e4

File tree

2 files changed

+277
-29
lines changed

2 files changed

+277
-29
lines changed

compiler/rustc_pattern_analysis/src/usefulness.rs

+205-29
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,166 @@
300300
//!
301301
//!
302302
//!
303+
//! # `Missing` and relevancy
304+
//!
305+
//! ## Relevant values
306+
//!
307+
//! Take the following example:
308+
//!
309+
//! ```compile_fail,E0004
310+
//! # let foo = (true, true);
311+
//! match foo {
312+
//! (true, _) => 1,
313+
//! (_, true) => 2,
314+
//! };
315+
//! ```
316+
//!
317+
//! Consider the value `(true, true)`:
318+
//! - Row 2 does not distinguish `(true, true)` and `(false, true)`;
319+
//! - `false` does not show up in the first column of the match, so without knowing anything else we
320+
//! can deduce that `(false, true)` matches the same or fewer rows than `(true, true)`.
321+
//!
322+
//! Using those two facts together, we deduce that `(true, true)` will not give us more usefulness
323+
//! information about row 2 than `(false, true)` would. We say that "`(true, true)` is made
324+
//! irrelevant for row 2 by `(false, true)`". We will use this idea to prune the search tree.
325+
//!
326+
//!
327+
//! ## Computing relevancy
328+
//!
329+
//! We now generalize from the above example to approximate relevancy in a simple way. Note that we
330+
//! will only compute an approximation: we can sometimes determine when a case is irrelevant, but
331+
//! computing this precisely is at least as hard as computing usefulness.
332+
//!
333+
//! Our computation of relevancy relies on the `Missing` constructor. As explained in
334+
//! [`crate::constructor`], `Missing` represents the constructors not present in a given column. For
335+
//! example in the following:
336+
//!
337+
//! ```compile_fail,E0004
338+
//! enum Direction { North, South, East, West }
339+
//! # let wind = (Direction::North, 0u8);
340+
//! match wind {
341+
//! (Direction::North, _) => 1,
342+
//! (_, 50..) => 2,
343+
//! };
344+
//! ```
345+
//!
346+
//! Here `South`, `East` and `West` are missing in the first column, and `0..50` is missing in the
347+
//! second. Both of these sets are represented by `Constructor::Missing` in their corresponding
348+
//! column.
349+
//!
350+
//! We then compute relevancy as follows: during the course of the algorithm, for a row `r`:
351+
//! - if `r` has a wildcard in the first column;
352+
//! - and some constructors are missing in that column;
353+
//! - then any `c != Missing` is considered irrelevant for row `r`.
354+
//!
355+
//! By this we mean that continuing the algorithm by specializing with `c` is guaranteed not to
356+
//! contribute more information about the usefulness of row `r` than what we would get by
357+
//! specializing with `Missing`. The argument is the same as in the previous subsection.
358+
//!
359+
//! Once we've specialized by a constructor `c` that is irrelevant for row `r`, we're guaranteed to
360+
//! only explore values irrelevant for `r`. If we then ever reach a point where we're only exploring
361+
//! values that are irrelevant to all of the rows (including the virtual wildcard row used for
362+
//! exhaustiveness), we skip that case entirely.
363+
//!
364+
//!
365+
//! ## Example
366+
//!
367+
//! Let's go through a variation on the first example:
368+
//!
369+
//! ```compile_fail,E0004
370+
//! # let foo = (true, true, true);
371+
//! match foo {
372+
//! (true, _, true) => 1,
373+
//! (_, true, _) => 2,
374+
//! };
375+
//! ```
376+
//!
377+
//! ```text
378+
//! ┐ Patterns:
379+
//! │ 1. `[(true, _, true)]`
380+
//! │ 2. `[(_, true, _)]`
381+
//! │ 3. `[_]` // virtual extra wildcard row
382+
//! │
383+
//! │ Specialize with `(,,)`:
384+
//! ├─┐ Patterns:
385+
//! │ │ 1. `[true, _, true]`
386+
//! │ │ 2. `[_, true, _]`
387+
//! │ │ 3. `[_, _, _]`
388+
//! │ │
389+
//! │ │ There are missing constructors in the first column (namely `false`), hence
390+
//! │ │ `true` is irrelevant for rows 2 and 3.
391+
//! │ │
392+
//! │ │ Specialize with `true`:
393+
//! │ ├─┐ Patterns:
394+
//! │ │ │ 1. `[_, true]`
395+
//! │ │ │ 2. `[true, _]` // now exploring irrelevant cases
396+
//! │ │ │ 3. `[_, _]` // now exploring irrelevant cases
397+
//! │ │ │
398+
//! │ │ │ There are missing constructors in the first column (namely `false`), hence
399+
//! │ │ │ `true` is irrelevant for rows 1 and 3.
400+
//! │ │ │
401+
//! │ │ │ Specialize with `true`:
402+
//! │ │ ├─┐ Patterns:
403+
//! │ │ │ │ 1. `[true]` // now exploring irrelevant cases
404+
//! │ │ │ │ 2. `[_]` // now exploring irrelevant cases
405+
//! │ │ │ │ 3. `[_]` // now exploring irrelevant cases
406+
//! │ │ │ │
407+
//! │ │ │ │ The current case is irrelevant for all rows: we backtrack immediately.
408+
//! │ │ ├─┘
409+
//! │ │ │
410+
//! │ │ │ Specialize with `false`:
411+
//! │ │ ├─┐ Patterns:
412+
//! │ │ │ │ 1. `[true]`
413+
//! │ │ │ │ 3. `[_]` // now exploring irrelevant cases
414+
//! │ │ │ │
415+
//! │ │ │ │ Specialize with `true`:
416+
//! │ │ │ ├─┐ Patterns:
417+
//! │ │ │ │ │ 1. `[]`
418+
//! │ │ │ │ │ 3. `[]` // now exploring irrelevant cases
419+
//! │ │ │ │ │
420+
//! │ │ │ │ │ Row 1 is therefore useful.
421+
//! │ │ │ ├─┘
422+
//! <etc...>
423+
//! ```
424+
//!
425+
//! Relevancy allowed us to skip the case `(true, true, _)` entirely. In some cases this pruning can
426+
//! give drastic speedups. The case this was built for is the following (#118437):
427+
//!
428+
//! ```ignore(illustrative)
429+
//! match foo {
430+
//! (true, _, _, _, ..) => 1,
431+
//! (_, true, _, _, ..) => 2,
432+
//! (_, _, true, _, ..) => 3,
433+
//! (_, _, _, true, ..) => 4,
434+
//! ...
435+
//! }
436+
//! ```
437+
//!
438+
//! Without considering relevancy, we would explore all 2^n combinations of the `true` and `Missing`
439+
//! constructors. Relevancy tells us that e.g. `(true, true, false, false, false, ...)` is
440+
//! irrelevant for all the rows. This allows us to skip all cases with more than one `true`
441+
//! constructor, changing the runtime from exponential to linear.
442+
//!
443+
//!
444+
//! ## Relevancy and exhaustiveness
445+
//!
446+
//! For exhaustiveness, we do something slightly different w.r.t relevancy: we do not report
447+
//! witnesses of non-exhaustiveness that are irrelevant for the virtual wildcard row. For example,
448+
//! in:
449+
//!
450+
//! ```ignore(illustrative)
451+
//! match foo {
452+
//! (true, true) => {}
453+
//! }
454+
//! ```
455+
//!
456+
//! we only report `(false, _)` as missing. This was a deliberate choice made early in the
457+
//! development of rust, for diagnostic and performance purposes. As showed in the previous section,
458+
//! ignoring irrelevant cases preserves usefulness, so this choice still correctly computes whether
459+
//! a match is exhaustive.
460+
//!
461+
//!
462+
//!
303463
//! # Or-patterns
304464
//!
305465
//! What we have described so far works well if there are no or-patterns. To handle them, if the
@@ -669,11 +829,15 @@ impl fmt::Display for ValidityConstraint {
669829
struct PatStack<'a, 'p, Cx: TypeCx> {
670830
// Rows of len 1 are very common, which is why `SmallVec[_; 2]` works well.
671831
pats: SmallVec<[&'a DeconstructedPat<'p, Cx>; 2]>,
832+
/// Sometimes we know that as far as this row is concerned, the current case is already handled
833+
/// by a different, more general, case. When the case is irrelevant for all rows this allows us
834+
/// to skip a case entirely. This is purely an optimization. See at the top for details.
835+
relevant: bool,
672836
}
673837

674838
impl<'a, 'p, Cx: TypeCx> PatStack<'a, 'p, Cx> {
675839
fn from_pattern(pat: &'a DeconstructedPat<'p, Cx>) -> Self {
676-
PatStack { pats: smallvec![pat] }
840+
PatStack { pats: smallvec![pat], relevant: true }
677841
}
678842

679843
fn is_empty(&self) -> bool {
@@ -708,12 +872,17 @@ impl<'a, 'p, Cx: TypeCx> PatStack<'a, 'p, Cx> {
708872
&self,
709873
pcx: &PlaceCtxt<'a, 'p, Cx>,
710874
ctor: &Constructor<Cx>,
875+
ctor_is_relevant: bool,
711876
) -> PatStack<'a, 'p, Cx> {
712877
// We pop the head pattern and push the new fields extracted from the arguments of
713878
// `self.head()`.
714879
let mut new_pats = self.head().specialize(pcx, ctor);
715880
new_pats.extend_from_slice(&self.pats[1..]);
716-
PatStack { pats: new_pats }
881+
// `ctor` is relevant for this row if it is the actual constructor of this row, or if the
882+
// row has a wildcard and `ctor` is relevant for wildcards.
883+
let ctor_is_relevant =
884+
!matches!(self.head().ctor(), Constructor::Wildcard) || ctor_is_relevant;
885+
PatStack { pats: new_pats, relevant: self.relevant && ctor_is_relevant }
717886
}
718887
}
719888

@@ -779,10 +948,11 @@ impl<'a, 'p, Cx: TypeCx> MatrixRow<'a, 'p, Cx> {
779948
&self,
780949
pcx: &PlaceCtxt<'a, 'p, Cx>,
781950
ctor: &Constructor<Cx>,
951+
ctor_is_relevant: bool,
782952
parent_row: usize,
783953
) -> MatrixRow<'a, 'p, Cx> {
784954
MatrixRow {
785-
pats: self.pats.pop_head_constructor(pcx, ctor),
955+
pats: self.pats.pop_head_constructor(pcx, ctor, ctor_is_relevant),
786956
parent_row,
787957
is_under_guard: self.is_under_guard,
788958
useful: false,
@@ -897,8 +1067,9 @@ impl<'a, 'p, Cx: TypeCx> Matrix<'a, 'p, Cx> {
8971067
&self,
8981068
pcx: &PlaceCtxt<'a, 'p, Cx>,
8991069
ctor: &Constructor<Cx>,
1070+
ctor_is_relevant: bool,
9001071
) -> Matrix<'a, 'p, Cx> {
901-
let wildcard_row = self.wildcard_row.pop_head_constructor(pcx, ctor);
1072+
let wildcard_row = self.wildcard_row.pop_head_constructor(pcx, ctor, ctor_is_relevant);
9021073
let new_validity = self.place_validity[0].specialize(ctor);
9031074
let new_place_validity = std::iter::repeat(new_validity)
9041075
.take(ctor.arity(pcx))
@@ -908,7 +1079,7 @@ impl<'a, 'p, Cx: TypeCx> Matrix<'a, 'p, Cx> {
9081079
Matrix { rows: Vec::new(), wildcard_row, place_validity: new_place_validity };
9091080
for (i, row) in self.rows().enumerate() {
9101081
if ctor.is_covered_by(pcx, row.head().ctor()) {
911-
let new_row = row.pop_head_constructor(pcx, ctor, i);
1082+
let new_row = row.pop_head_constructor(pcx, ctor, ctor_is_relevant, i);
9121083
matrix.expand_and_push(new_row);
9131084
}
9141085
}
@@ -1108,7 +1279,10 @@ impl<Cx: TypeCx> WitnessMatrix<Cx> {
11081279
if matches!(ctor, Constructor::Missing) {
11091280
// We got the special `Missing` constructor that stands for the constructors not present
11101281
// in the match.
1111-
if !report_individual_missing_ctors {
1282+
if missing_ctors.is_empty() {
1283+
// Nothing to report.
1284+
*self = Self::empty();
1285+
} else if !report_individual_missing_ctors {
11121286
// Report `_` as missing.
11131287
let pat = WitnessPat::wild_from_ctor(pcx, Constructor::Wildcard);
11141288
self.push_pattern(pat);
@@ -1167,6 +1341,13 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
11671341
) -> WitnessMatrix<Cx> {
11681342
debug_assert!(matrix.rows().all(|r| r.len() == matrix.column_count()));
11691343

1344+
if !matrix.wildcard_row.relevant && matrix.rows().all(|r| !r.pats.relevant) {
1345+
// Here we know that nothing will contribute further to exhaustiveness or usefulness. This
1346+
// is purely an optimization: skipping this check doesn't affect correctness. See the top of
1347+
// the file for details.
1348+
return WitnessMatrix::empty();
1349+
}
1350+
11701351
let Some(ty) = matrix.head_ty(mcx) else {
11711352
// The base case: there are no columns in the matrix. We are morally pattern-matching on ().
11721353
// A row is useful iff it has no (unguarded) rows above it.
@@ -1179,8 +1360,14 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
11791360
return WitnessMatrix::empty();
11801361
}
11811362
}
1182-
// No (unguarded) rows, so the match is not exhaustive. We return a new witness.
1183-
return WitnessMatrix::unit_witness();
1363+
// No (unguarded) rows, so the match is not exhaustive. We return a new witness unless
1364+
// irrelevant.
1365+
return if matrix.wildcard_row.relevant {
1366+
WitnessMatrix::unit_witness()
1367+
} else {
1368+
// We choose to not report anything here; see at the top for details.
1369+
WitnessMatrix::empty()
1370+
};
11841371
};
11851372

11861373
debug!("ty: {ty:?}");
@@ -1223,32 +1410,21 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
12231410

12241411
let mut ret = WitnessMatrix::empty();
12251412
for ctor in split_ctors {
1226-
debug!("specialize({:?})", ctor);
12271413
// Dig into rows that match `ctor`.
1228-
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor);
1414+
debug!("specialize({:?})", ctor);
1415+
// `ctor` is *irrelevant* if there's another constructor in `split_ctors` that matches
1416+
// strictly fewer rows. In that case we can sometimes skip it. See the top of the file for
1417+
// details.
1418+
let ctor_is_relevant = matches!(ctor, Constructor::Missing) || missing_ctors.is_empty();
1419+
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor, ctor_is_relevant);
12291420
let mut witnesses = ensure_sufficient_stack(|| {
12301421
compute_exhaustiveness_and_usefulness(mcx, &mut spec_matrix, false)
12311422
});
12321423

1233-
let counts_for_exhaustiveness = match ctor {
1234-
Constructor::Missing => !missing_ctors.is_empty(),
1235-
// If there are missing constructors we'll report those instead. Since `Missing` matches
1236-
// only the wildcard rows, it matches fewer rows than this constructor, and is therefore
1237-
// guaranteed to result in the same or more witnesses. So skipping this does not
1238-
// jeopardize correctness.
1239-
_ => missing_ctors.is_empty(),
1240-
};
1241-
if counts_for_exhaustiveness {
1242-
// Transform witnesses for `spec_matrix` into witnesses for `matrix`.
1243-
witnesses.apply_constructor(
1244-
pcx,
1245-
&missing_ctors,
1246-
&ctor,
1247-
report_individual_missing_ctors,
1248-
);
1249-
// Accumulate the found witnesses.
1250-
ret.extend(witnesses);
1251-
}
1424+
// Transform witnesses for `spec_matrix` into witnesses for `matrix`.
1425+
witnesses.apply_constructor(pcx, &missing_ctors, &ctor, report_individual_missing_ctors);
1426+
// Accumulate the found witnesses.
1427+
ret.extend(witnesses);
12521428

12531429
// A parent row is useful if any of its children is.
12541430
for child_row in spec_matrix.rows() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// check-pass
2+
struct BaseCommand {
3+
field01: bool,
4+
field02: bool,
5+
field03: bool,
6+
field04: bool,
7+
field05: bool,
8+
field06: bool,
9+
field07: bool,
10+
field08: bool,
11+
field09: bool,
12+
field10: bool,
13+
field11: bool,
14+
field12: bool,
15+
field13: bool,
16+
field14: bool,
17+
field15: bool,
18+
field16: bool,
19+
field17: bool,
20+
field18: bool,
21+
field19: bool,
22+
field20: bool,
23+
field21: bool,
24+
field22: bool,
25+
field23: bool,
26+
field24: bool,
27+
field25: bool,
28+
field26: bool,
29+
field27: bool,
30+
field28: bool,
31+
field29: bool,
32+
field30: bool,
33+
}
34+
35+
fn request_key(command: BaseCommand) {
36+
match command {
37+
BaseCommand { field01: true, .. } => {}
38+
BaseCommand { field02: true, .. } => {}
39+
BaseCommand { field03: true, .. } => {}
40+
BaseCommand { field04: true, .. } => {}
41+
BaseCommand { field05: true, .. } => {}
42+
BaseCommand { field06: true, .. } => {}
43+
BaseCommand { field07: true, .. } => {}
44+
BaseCommand { field08: true, .. } => {}
45+
BaseCommand { field09: true, .. } => {}
46+
BaseCommand { field10: true, .. } => {}
47+
BaseCommand { field11: true, .. } => {}
48+
BaseCommand { field12: true, .. } => {}
49+
BaseCommand { field13: true, .. } => {}
50+
BaseCommand { field14: true, .. } => {}
51+
BaseCommand { field15: true, .. } => {}
52+
BaseCommand { field16: true, .. } => {}
53+
BaseCommand { field17: true, .. } => {}
54+
BaseCommand { field18: true, .. } => {}
55+
BaseCommand { field19: true, .. } => {}
56+
BaseCommand { field20: true, .. } => {}
57+
BaseCommand { field21: true, .. } => {}
58+
BaseCommand { field22: true, .. } => {}
59+
BaseCommand { field23: true, .. } => {}
60+
BaseCommand { field24: true, .. } => {}
61+
BaseCommand { field25: true, .. } => {}
62+
BaseCommand { field26: true, .. } => {}
63+
BaseCommand { field27: true, .. } => {}
64+
BaseCommand { field28: true, .. } => {}
65+
BaseCommand { field29: true, .. } => {}
66+
BaseCommand { field30: true, .. } => {}
67+
68+
_ => {}
69+
}
70+
}
71+
72+
fn main() {}

0 commit comments

Comments
 (0)