Rollup merge of #121374 - Nadrieril:factor-explain, r=matthewjasper

match lowering: Split off `test_candidates` into several functions and improve comments

The logic of `test_candidates` has three steps: pick a test, sort the candidates, and generate code for everything. So I split it off into three methods.

I also ended up reworking the comments that explain the algorithm. In particular I added detailed examples. I removed the digression about https://github.com/rust-lang/rust/issues/29740 because it's no longer relevant to how the code is structured today.

r? ``@matthewjasper``
This commit is contained in:
Matthias Krüger 2024-02-24 22:38:58 +01:00 committed by GitHub
commit f4ba47f1ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1150,39 +1150,61 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
/// the value, we will set and generate a branch to the appropriate /// the value, we will set and generate a branch to the appropriate
/// pre-binding block. /// pre-binding block.
/// ///
/// If we find that *NONE* of the candidates apply, we branch to the /// If we find that *NONE* of the candidates apply, we branch to `otherwise_block`.
/// `otherwise_block`, setting it to `Some` if required. In principle, this
/// means that the input list was not exhaustive, though at present we
/// sometimes are not smart enough to recognize all exhaustive inputs.
/// ///
/// It might be surprising that the input can be non-exhaustive. /// It might be surprising that the input can be non-exhaustive.
/// Indeed, initially, it is not, because all matches are /// Indeed, initially, it is not, because all matches are
/// exhaustive in Rust. But during processing we sometimes divide /// exhaustive in Rust. But during processing we sometimes divide
/// up the list of candidates and recurse with a non-exhaustive /// up the list of candidates and recurse with a non-exhaustive
/// list. This is important to keep the size of the generated code /// list. This is how our lowering approach (called "backtracking
/// under control. See [`Builder::test_candidates`] for more details. /// automaton" in the literature) works.
/// See [`Builder::test_candidates`] for more details.
/// ///
/// If `fake_borrows` is `Some`, then places which need fake borrows /// If `fake_borrows` is `Some`, then places which need fake borrows
/// will be added to it. /// will be added to it.
/// ///
/// For an example of a case where we set `otherwise_block`, even for an /// For an example of how we use `otherwise_block`, consider:
/// exhaustive match, consider:
///
/// ``` /// ```
/// # fn foo(x: (bool, bool)) { /// # fn foo((x, y): (bool, bool)) -> u32 {
/// match x { /// match (x, y) {
/// (true, true) => (), /// (true, true) => 1,
/// (_, false) => (), /// (_, false) => 2,
/// (false, true) => (), /// (false, true) => 3,
/// } /// }
/// # } /// # }
/// ``` /// ```
/// For this match, we generate something like:
/// ```
/// # fn foo((x, y): (bool, bool)) -> u32 {
/// if x {
/// if y {
/// return 1
/// } else {
/// // continue
/// }
/// } else {
/// // continue
/// }
/// if y {
/// if x {
/// // This is actually unreachable because the `(true, true)` case was handled above.
/// // continue
/// } else {
/// return 3
/// }
/// } else {
/// return 2
/// }
/// // this is the final `otherwise_block`, which is unreachable because the match was exhaustive.
/// unreachable!()
/// # }
/// ```
/// ///
/// For this match, we check if `x.0` matches `true` (for the first /// Every `continue` is an instance of branching to some `otherwise_block` somewhere deep within
/// arm). If it doesn't match, we check `x.1`. If `x.1` is `true` we check /// the algorithm. For more details on why we lower like this, see [`Builder::test_candidates`].
/// if `x.0` matches `false` (for the third arm). In the (impossible at ///
/// runtime) case when `x.0` is now `true`, we branch to /// Note how we test `x` twice. This is the tradeoff of backtracking automata: we prefer smaller
/// `otherwise_block`. /// code size at the expense of non-optimal code paths.
#[instrument(skip(self, fake_borrows), level = "debug")] #[instrument(skip(self, fake_borrows), level = "debug")]
fn match_candidates<'pat>( fn match_candidates<'pat>(
&mut self, &mut self,
@ -1557,18 +1579,12 @@ fn merge_trivial_subcandidates(
} }
} }
/// This is the most subtle part of the matching algorithm. At /// Pick a test to run. Which test doesn't matter as long as it is guaranteed to fully match at
/// this point, the input candidates have been fully simplified, /// least one match pair. We currently simply pick the test corresponding to the first match
/// and so we know that all remaining match-pairs require some /// pair of the first candidate in the list.
/// sort of test. To decide what test to perform, we take the highest
/// priority candidate (the first one in the list, as of January 2021)
/// and extract the first match-pair from the list. From this we decide
/// what kind of test is needed using [`Builder::test`], defined in the
/// [`test` module](mod@test).
/// ///
/// *Note:* taking the first match pair is somewhat arbitrary, and /// *Note:* taking the first match pair is somewhat arbitrary, and we might do better here by
/// we might do better here by choosing more carefully what to /// choosing more carefully what to test.
/// test.
/// ///
/// For example, consider the following possible match-pairs: /// For example, consider the following possible match-pairs:
/// ///
@ -1580,121 +1596,19 @@ fn merge_trivial_subcandidates(
/// [`Switch`]: TestKind::Switch /// [`Switch`]: TestKind::Switch
/// [`SwitchInt`]: TestKind::SwitchInt /// [`SwitchInt`]: TestKind::SwitchInt
/// [`Range`]: TestKind::Range /// [`Range`]: TestKind::Range
/// fn pick_test(
/// Once we know what sort of test we are going to perform, this
/// test may also help us winnow down our candidates. So we walk over
/// the candidates (from high to low priority) and check. This
/// gives us, for each outcome of the test, a transformed list of
/// candidates. For example, if we are testing `x.0`'s variant,
/// and we have a candidate `(x.0 @ Some(v), x.1 @ 22)`,
/// then we would have a resulting candidate of `((x.0 as Some).0 @ v, x.1 @ 22)`.
/// Note that the first match-pair is now simpler (and, in fact, irrefutable).
///
/// But there may also be candidates that the test just doesn't
/// apply to. The classical example involves wildcards:
///
/// ```
/// # let (x, y, z) = (true, true, true);
/// match (x, y, z) {
/// (true , _ , true ) => true, // (0)
/// (_ , true , _ ) => true, // (1)
/// (false, false, _ ) => false, // (2)
/// (true , _ , false) => false, // (3)
/// }
/// # ;
/// ```
///
/// In that case, after we test on `x`, there are 2 overlapping candidate
/// sets:
///
/// - If the outcome is that `x` is true, candidates 0, 1, and 3
/// - If the outcome is that `x` is false, candidates 1 and 2
///
/// Here, the traditional "decision tree" method would generate 2
/// separate code-paths for the 2 separate cases.
///
/// In some cases, this duplication can create an exponential amount of
/// code. This is most easily seen by noticing that this method terminates
/// with precisely the reachable arms being reachable - but that problem
/// is trivially NP-complete:
///
/// ```ignore (illustrative)
/// match (var0, var1, var2, var3, ...) {
/// (true , _ , _ , false, true, ...) => false,
/// (_ , true, true , false, _ , ...) => false,
/// (false, _ , false, false, _ , ...) => false,
/// ...
/// _ => true
/// }
/// ```
///
/// Here the last arm is reachable only if there is an assignment to
/// the variables that does not match any of the literals. Therefore,
/// compilation would take an exponential amount of time in some cases.
///
/// That kind of exponential worst-case might not occur in practice, but
/// our simplistic treatment of constants and guards would make it occur
/// in very common situations - for example [#29740]:
///
/// ```ignore (illustrative)
/// match x {
/// "foo" if foo_guard => ...,
/// "bar" if bar_guard => ...,
/// "baz" if baz_guard => ...,
/// ...
/// }
/// ```
///
/// [#29740]: https://github.com/rust-lang/rust/issues/29740
///
/// Here we first test the match-pair `x @ "foo"`, which is an [`Eq` test].
///
/// [`Eq` test]: TestKind::Eq
///
/// It might seem that we would end up with 2 disjoint candidate
/// sets, consisting of the first candidate or the other two, but our
/// algorithm doesn't reason about `"foo"` being distinct from the other
/// constants; it considers the latter arms to potentially match after
/// both outcomes, which obviously leads to an exponential number
/// of tests.
///
/// To avoid these kinds of problems, our algorithm tries to ensure
/// the amount of generated tests is linear. When we do a k-way test,
/// we return an additional "unmatched" set alongside the obvious `k`
/// sets. When we encounter a candidate that would be present in more
/// than one of the sets, we put it and all candidates below it into the
/// "unmatched" set. This ensures these `k+1` sets are disjoint.
///
/// After we perform our test, we branch into the appropriate candidate
/// set and recurse with `match_candidates`. These sub-matches are
/// obviously non-exhaustive - as we discarded our otherwise set - so
/// we set their continuation to do `match_candidates` on the
/// "unmatched" set (which is again non-exhaustive).
///
/// If you apply this to the above test, you basically wind up
/// with an if-else-if chain, testing each candidate in turn,
/// which is precisely what we want.
///
/// In addition to avoiding exponential-time blowups, this algorithm
/// also has the nice property that each guard and arm is only generated
/// once.
fn test_candidates<'pat, 'b, 'c>(
&mut self, &mut self,
span: Span, candidates: &mut [&mut Candidate<'_, 'tcx>],
scrutinee_span: Span,
mut candidates: &'b mut [&'c mut Candidate<'pat, 'tcx>],
start_block: BasicBlock,
otherwise_block: BasicBlock,
fake_borrows: &mut Option<FxIndexSet<Place<'tcx>>>, fake_borrows: &mut Option<FxIndexSet<Place<'tcx>>>,
) { ) -> (PlaceBuilder<'tcx>, Test<'tcx>) {
// extract the match-pair from the highest priority candidate // Extract the match-pair from the highest priority candidate
let match_pair = &candidates.first().unwrap().match_pairs[0]; let match_pair = &candidates.first().unwrap().match_pairs[0];
let mut test = self.test(match_pair); let mut test = self.test(match_pair);
let match_place = match_pair.place.clone(); let match_place = match_pair.place.clone();
// most of the time, the test to perform is simply a function debug!(?test, ?match_pair);
// of the main candidate; but for a test like SwitchInt, we // Most of the time, the test to perform is simply a function of the main candidate; but for
// may want to add cases based on the candidates that are // a test like SwitchInt, we may want to add cases based on the candidates that are
// available // available
match test.kind { match test.kind {
TestKind::SwitchInt { switch_ty: _, ref mut options } => { TestKind::SwitchInt { switch_ty: _, ref mut options } => {
@ -1721,20 +1635,58 @@ fn test_candidates<'pat, 'b, 'c>(
fb.insert(resolved_place); fb.insert(resolved_place);
} }
// perform the test, branching to one of N blocks. For each of (match_place, test)
// those N possible outcomes, create a (initially empty) }
// vector of candidates. Those are the candidates that still
// apply if the test has that particular outcome. /// Given a test, we sort the input candidates into several buckets. If a candidate only matches
debug!("test_candidates: test={:?} match_pair={:?}", test, match_pair); /// in one of the branches of `test`, we move it there. If it could match in more than one of
/// the branches of `test`, we stop sorting candidates.
///
/// This returns a pair of
/// - the candidates that weren't sorted;
/// - for each possible outcome of the test, the candidates that match in that outcome.
///
/// Moreover, we transform the branched candidates to reflect the fact that we know which
/// outcome of `test` occurred.
///
/// For example:
/// ```
/// # let (x, y, z) = (true, true, true);
/// match (x, y, z) {
/// (true , _ , true ) => true, // (0)
/// (false, false, _ ) => false, // (1)
/// (_ , true , _ ) => true, // (2)
/// (true , _ , false) => false, // (3)
/// }
/// # ;
/// ```
///
/// Assume we are testing on `x`. There are 2 overlapping candidate sets:
/// - If the outcome is that `x` is true, candidates 0, 2, and 3
/// - If the outcome is that `x` is false, candidates 1 and 2
///
/// Following our algorithm, candidate 0 is sorted into outcome `x == true`, candidate 1 goes
/// into outcome `x == false`, and candidate 2 and 3 remain unsorted.
///
/// The sorted candidates are transformed:
/// - candidate 0 becomes `[z @ true]` since we know that `x` was `true`;
/// - candidate 1 becomes `[y @ false]` since we know that `x` was `false`.
fn sort_candidates<'b, 'c, 'pat>(
&mut self,
match_place: &PlaceBuilder<'tcx>,
test: &Test<'tcx>,
mut candidates: &'b mut [&'c mut Candidate<'pat, 'tcx>],
) -> (&'b mut [&'c mut Candidate<'pat, 'tcx>], Vec<Vec<&'b mut Candidate<'pat, 'tcx>>>) {
// For each of the N possible outcomes, create a (initially empty) vector of candidates.
// Those are the candidates that apply if the test has that particular outcome.
let mut target_candidates: Vec<Vec<&mut Candidate<'pat, 'tcx>>> = vec![]; let mut target_candidates: Vec<Vec<&mut Candidate<'pat, 'tcx>>> = vec![];
target_candidates.resize_with(test.targets(), Default::default); target_candidates.resize_with(test.targets(), Default::default);
let total_candidate_count = candidates.len(); let total_candidate_count = candidates.len();
// Sort the candidates into the appropriate vector in // Sort the candidates into the appropriate vector in `target_candidates`. Note that at some
// `target_candidates`. Note that at some point we may // point we may encounter a candidate where the test is not relevant; at that point, we stop
// encounter a candidate where the test is not relevant; at // sorting.
// that point, we stop sorting.
while let Some(candidate) = candidates.first_mut() { while let Some(candidate) = candidates.first_mut() {
let Some(idx) = self.sort_candidate(&match_place, &test, candidate) else { let Some(idx) = self.sort_candidate(&match_place, &test, candidate) else {
break; break;
@ -1743,7 +1695,8 @@ fn test_candidates<'pat, 'b, 'c>(
target_candidates[idx].push(candidate); target_candidates[idx].push(candidate);
candidates = rest; candidates = rest;
} }
// at least the first candidate ought to be tested
// At least the first candidate ought to be tested
assert!( assert!(
total_candidate_count > candidates.len(), total_candidate_count > candidates.len(),
"{total_candidate_count}, {candidates:#?}" "{total_candidate_count}, {candidates:#?}"
@ -1751,16 +1704,130 @@ fn test_candidates<'pat, 'b, 'c>(
debug!("tested_candidates: {}", total_candidate_count - candidates.len()); debug!("tested_candidates: {}", total_candidate_count - candidates.len());
debug!("untested_candidates: {}", candidates.len()); debug!("untested_candidates: {}", candidates.len());
(candidates, target_candidates)
}
/// This is the most subtle part of the match lowering algorithm. At this point, the input
/// candidates have been fully simplified, so all remaining match-pairs require some sort of
/// test.
///
/// Once we pick what sort of test we are going to perform, this test will help us winnow down
/// our candidates. So we walk over the candidates (from high to low priority) and check. We
/// compute, for each outcome of the test, a transformed list of candidates. If a candidate
/// matches in a single branch of our test, we add it to the corresponding outcome. We also
/// transform it to record the fact that we know which outcome occurred.
///
/// For example, if we are testing `x.0`'s variant, and we have a candidate `(x.0 @ Some(v), x.1
/// @ 22)`, then we would have a resulting candidate of `((x.0 as Some).0 @ v, x.1 @ 22)` in the
/// branch corresponding to `Some`. To ensure we make progress, we always pick a test that
/// results in simplifying the first candidate.
///
/// But there may also be candidates that the test doesn't
/// apply to. The classical example is wildcards:
///
/// ```
/// # let (x, y, z) = (true, true, true);
/// match (x, y, z) {
/// (true , _ , true ) => true, // (0)
/// (false, false, _ ) => false, // (1)
/// (_ , true , _ ) => true, // (2)
/// (true , _ , false) => false, // (3)
/// }
/// # ;
/// ```
///
/// Here, the traditional "decision tree" method would generate 2 separate code-paths for the 2
/// possible values of `x`. This would however duplicate some candidates, which would need to be
/// lowered several times.
///
/// In some cases, this duplication can create an exponential amount of
/// code. This is most easily seen by noticing that this method terminates
/// with precisely the reachable arms being reachable - but that problem
/// is trivially NP-complete:
///
/// ```ignore (illustrative)
/// match (var0, var1, var2, var3, ...) {
/// (true , _ , _ , false, true, ...) => false,
/// (_ , true, true , false, _ , ...) => false,
/// (false, _ , false, false, _ , ...) => false,
/// ...
/// _ => true
/// }
/// ```
///
/// Here the last arm is reachable only if there is an assignment to
/// the variables that does not match any of the literals. Therefore,
/// compilation would take an exponential amount of time in some cases.
///
/// In rustc, we opt instead for the "backtracking automaton" approach. This guarantees we never
/// duplicate a candidate (except in the presence of or-patterns). In fact this guarantee is
/// ensured by the fact that we carry around `&mut Candidate`s which can't be duplicated.
///
/// To make this work, whenever we decide to perform a test, if we encounter a candidate that
/// could match in more than one branch of the test, we stop. We generate code for the test and
/// for the candidates in its branches; the remaining candidates will be tested if the
/// candidates in the branches fail to match.
///
/// For example, if we test on `x` in the following:
/// ```
/// # fn foo((x, y, z): (bool, bool, bool)) -> u32 {
/// match (x, y, z) {
/// (true , _ , true ) => 0,
/// (false, false, _ ) => 1,
/// (_ , true , _ ) => 2,
/// (true , _ , false) => 3,
/// }
/// # }
/// ```
/// this function generates code that looks more of less like:
/// ```
/// # fn foo((x, y, z): (bool, bool, bool)) -> u32 {
/// if x {
/// match (y, z) {
/// (_, true) => return 0,
/// _ => {} // continue matching
/// }
/// } else {
/// match (y, z) {
/// (false, _) => return 1,
/// _ => {} // continue matching
/// }
/// }
/// // the block here is `remainder_start`
/// match (x, y, z) {
/// (_ , true , _ ) => 2,
/// (true , _ , false) => 3,
/// _ => unreachable!(),
/// }
/// # }
/// ```
fn test_candidates<'pat, 'b, 'c>(
&mut self,
span: Span,
scrutinee_span: Span,
candidates: &'b mut [&'c mut Candidate<'pat, 'tcx>],
start_block: BasicBlock,
otherwise_block: BasicBlock,
fake_borrows: &mut Option<FxIndexSet<Place<'tcx>>>,
) {
// Extract the match-pair from the highest priority candidate and build a test from it.
let (match_place, test) = self.pick_test(candidates, fake_borrows);
// For each of the N possible test outcomes, build the vector of candidates that applies if
// the test has that particular outcome.
let (remaining_candidates, target_candidates) =
self.sort_candidates(&match_place, &test, candidates);
// The block that we should branch to if none of the // The block that we should branch to if none of the
// `target_candidates` match. // `target_candidates` match.
let remainder_start = if !candidates.is_empty() { let remainder_start = if !remaining_candidates.is_empty() {
let remainder_start = self.cfg.start_new_block(); let remainder_start = self.cfg.start_new_block();
self.match_candidates( self.match_candidates(
span, span,
scrutinee_span, scrutinee_span,
remainder_start, remainder_start,
otherwise_block, otherwise_block,
candidates, remaining_candidates,
fake_borrows, fake_borrows,
); );
remainder_start remainder_start