Skip to content

Commit d602710

Browse files
committed
Move match-candidate partitioning code into its own module
1 parent ce63e5d commit d602710

File tree

3 files changed

+353
-340
lines changed

3 files changed

+353
-340
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
use std::cmp::Ordering;
2+
3+
use rustc_data_structures::fx::FxIndexMap;
4+
use rustc_middle::mir::{BinOp, Place};
5+
use rustc_middle::span_bug;
6+
use tracing::debug;
7+
8+
use crate::builder::Builder;
9+
use crate::builder::matches::test::is_switch_ty;
10+
use crate::builder::matches::{Candidate, Test, TestBranch, TestCase, TestKind};
11+
12+
impl<'a, 'tcx> Builder<'a, 'tcx> {
13+
/// Given a test, we partition the input candidates into several buckets.
14+
/// If a candidate matches in exactly one of the branches of `test`
15+
/// (and no other branches), we put it into the corresponding bucket.
16+
/// If it could match in more than one of the branches of `test`, the test
17+
/// doesn't usefully apply to it, and we stop partitioning candidates.
18+
///
19+
/// Importantly, we also **mutate** the branched candidates to remove match pairs
20+
/// that are entailed by the outcome of the test, and add any sub-pairs of the
21+
/// removed pairs.
22+
///
23+
/// This returns a pair of
24+
/// - the candidates that weren't sorted;
25+
/// - for each possible outcome of the test, the candidates that match in that outcome.
26+
///
27+
/// For example:
28+
/// ```
29+
/// # let (x, y, z) = (true, true, true);
30+
/// match (x, y, z) {
31+
/// (true , _ , true ) => true, // (0)
32+
/// (false, false, _ ) => false, // (1)
33+
/// (_ , true , _ ) => true, // (2)
34+
/// (true , _ , false) => false, // (3)
35+
/// }
36+
/// # ;
37+
/// ```
38+
///
39+
/// Assume we are testing on `x`. Conceptually, there are 2 overlapping candidate sets:
40+
/// - If the outcome is that `x` is true, candidates {0, 2, 3} are possible
41+
/// - If the outcome is that `x` is false, candidates {1, 2} are possible
42+
///
43+
/// Following our algorithm:
44+
/// - Candidate 0 is sorted into outcome `x == true`
45+
/// - Candidate 1 is sorted into outcome `x == false`
46+
/// - Candidate 2 remains unsorted, because testing `x` has no effect on it
47+
/// - Candidate 3 remains unsorted, because a previous candidate (2) was unsorted
48+
/// - This helps preserve the illusion that candidates are tested "in order"
49+
///
50+
/// The sorted candidates are mutated to remove entailed match pairs:
51+
/// - candidate 0 becomes `[z @ true]` since we know that `x` was `true`;
52+
/// - candidate 1 becomes `[y @ false]` since we know that `x` was `false`.
53+
pub(super) fn sort_candidates<'b, 'c>(
54+
&mut self,
55+
match_place: Place<'tcx>,
56+
test: &Test<'tcx>,
57+
mut candidates: &'b mut [&'c mut Candidate<'tcx>],
58+
) -> (
59+
&'b mut [&'c mut Candidate<'tcx>],
60+
FxIndexMap<TestBranch<'tcx>, Vec<&'b mut Candidate<'tcx>>>,
61+
) {
62+
// For each of the possible outcomes, collect vector of candidates that apply if the test
63+
// has that particular outcome.
64+
let mut target_candidates: FxIndexMap<_, Vec<&mut Candidate<'_>>> = Default::default();
65+
66+
let total_candidate_count = candidates.len();
67+
68+
// Sort the candidates into the appropriate vector in `target_candidates`. Note that at some
69+
// point we may encounter a candidate where the test is not relevant; at that point, we stop
70+
// sorting.
71+
while let Some(candidate) = candidates.first_mut() {
72+
let Some(branch) =
73+
self.sort_candidate(match_place, test, candidate, &target_candidates)
74+
else {
75+
break;
76+
};
77+
let (candidate, rest) = candidates.split_first_mut().unwrap();
78+
target_candidates.entry(branch).or_insert_with(Vec::new).push(candidate);
79+
candidates = rest;
80+
}
81+
82+
// At least the first candidate ought to be tested
83+
assert!(
84+
total_candidate_count > candidates.len(),
85+
"{total_candidate_count}, {candidates:#?}"
86+
);
87+
debug!("tested_candidates: {}", total_candidate_count - candidates.len());
88+
debug!("untested_candidates: {}", candidates.len());
89+
90+
(candidates, target_candidates)
91+
}
92+
93+
/// Given that we are performing `test` against `test_place`, this job
94+
/// sorts out what the status of `candidate` will be after the test. See
95+
/// `test_candidates` for the usage of this function. The candidate may
96+
/// be modified to update its `match_pairs`.
97+
///
98+
/// So, for example, if this candidate is `x @ Some(P0)` and the `Test` is
99+
/// a variant test, then we would modify the candidate to be `(x as
100+
/// Option).0 @ P0` and return the index corresponding to the variant
101+
/// `Some`.
102+
///
103+
/// However, in some cases, the test may just not be relevant to candidate.
104+
/// For example, suppose we are testing whether `foo.x == 22`, but in one
105+
/// match arm we have `Foo { x: _, ... }`... in that case, the test for
106+
/// the value of `x` has no particular relevance to this candidate. In
107+
/// such cases, this function just returns None without doing anything.
108+
/// This is used by the overall `match_candidates` algorithm to structure
109+
/// the match as a whole. See `match_candidates` for more details.
110+
///
111+
/// FIXME(#29623). In some cases, we have some tricky choices to make. for
112+
/// example, if we are testing that `x == 22`, but the candidate is `x @
113+
/// 13..55`, what should we do? In the event that the test is true, we know
114+
/// that the candidate applies, but in the event of false, we don't know
115+
/// that it *doesn't* apply. For now, we return false, indicate that the
116+
/// test does not apply to this candidate, but it might be we can get
117+
/// tighter match code if we do something a bit different.
118+
fn sort_candidate(
119+
&mut self,
120+
test_place: Place<'tcx>,
121+
test: &Test<'tcx>,
122+
candidate: &mut Candidate<'tcx>,
123+
sorted_candidates: &FxIndexMap<TestBranch<'tcx>, Vec<&mut Candidate<'tcx>>>,
124+
) -> Option<TestBranch<'tcx>> {
125+
// Find the match_pair for this place (if any). At present,
126+
// afaik, there can be at most one. (In the future, if we
127+
// adopted a more general `@` operator, there might be more
128+
// than one, but it'd be very unusual to have two sides that
129+
// both require tests; you'd expect one side to be simplified
130+
// away.)
131+
let (match_pair_index, match_pair) = candidate
132+
.match_pairs
133+
.iter()
134+
.enumerate()
135+
.find(|&(_, mp)| mp.place == Some(test_place))?;
136+
137+
// If true, the match pair is completely entailed by its corresponding test
138+
// branch, so it can be removed. If false, the match pair is _compatible_
139+
// with its test branch, but still needs a more specific test.
140+
let fully_matched;
141+
let ret = match (&test.kind, &match_pair.test_case) {
142+
// If we are performing a variant switch, then this
143+
// informs variant patterns, but nothing else.
144+
(
145+
&TestKind::Switch { adt_def: tested_adt_def },
146+
&TestCase::Variant { adt_def, variant_index },
147+
) => {
148+
assert_eq!(adt_def, tested_adt_def);
149+
fully_matched = true;
150+
Some(TestBranch::Variant(variant_index))
151+
}
152+
153+
// If we are performing a switch over integers, then this informs integer
154+
// equality, but nothing else.
155+
//
156+
// FIXME(#29623) we could use PatKind::Range to rule
157+
// things out here, in some cases.
158+
(TestKind::SwitchInt, &TestCase::Constant { value })
159+
if is_switch_ty(match_pair.pattern_ty) =>
160+
{
161+
// An important invariant of candidate sorting is that a candidate
162+
// must not match in multiple branches. For `SwitchInt` tests, adding
163+
// a new value might invalidate that property for range patterns that
164+
// have already been sorted into the failure arm, so we must take care
165+
// not to add such values here.
166+
let is_covering_range = |test_case: &TestCase<'tcx>| {
167+
test_case.as_range().is_some_and(|range| {
168+
matches!(range.contains(value, self.tcx), None | Some(true))
169+
})
170+
};
171+
let is_conflicting_candidate = |candidate: &&mut Candidate<'tcx>| {
172+
candidate
173+
.match_pairs
174+
.iter()
175+
.any(|mp| mp.place == Some(test_place) && is_covering_range(&mp.test_case))
176+
};
177+
if sorted_candidates
178+
.get(&TestBranch::Failure)
179+
.is_some_and(|candidates| candidates.iter().any(is_conflicting_candidate))
180+
{
181+
fully_matched = false;
182+
None
183+
} else {
184+
fully_matched = true;
185+
Some(TestBranch::Constant(value))
186+
}
187+
}
188+
(TestKind::SwitchInt, TestCase::Range(range)) => {
189+
// When performing a `SwitchInt` test, a range pattern can be
190+
// sorted into the failure arm if it doesn't contain _any_ of
191+
// the values being tested. (This restricts what values can be
192+
// added to the test by subsequent candidates.)
193+
fully_matched = false;
194+
let not_contained = sorted_candidates
195+
.keys()
196+
.filter_map(|br| br.as_constant())
197+
.all(|val| matches!(range.contains(val, self.tcx), Some(false)));
198+
199+
not_contained.then(|| {
200+
// No switch values are contained in the pattern range,
201+
// so the pattern can be matched only if this test fails.
202+
TestBranch::Failure
203+
})
204+
}
205+
206+
(TestKind::If, TestCase::Constant { value }) => {
207+
fully_matched = true;
208+
let value = value.try_to_bool().unwrap_or_else(|| {
209+
span_bug!(test.span, "expected boolean value but got {value:?}")
210+
});
211+
Some(if value { TestBranch::Success } else { TestBranch::Failure })
212+
}
213+
214+
(
215+
&TestKind::Len { len: test_len, op: BinOp::Eq },
216+
&TestCase::Slice { len, variable_length },
217+
) => {
218+
match (test_len.cmp(&(len as u64)), variable_length) {
219+
(Ordering::Equal, false) => {
220+
// on true, min_len = len = $actual_length,
221+
// on false, len != $actual_length
222+
fully_matched = true;
223+
Some(TestBranch::Success)
224+
}
225+
(Ordering::Less, _) => {
226+
// test_len < pat_len. If $actual_len = test_len,
227+
// then $actual_len < pat_len and we don't have
228+
// enough elements.
229+
fully_matched = false;
230+
Some(TestBranch::Failure)
231+
}
232+
(Ordering::Equal | Ordering::Greater, true) => {
233+
// This can match both if $actual_len = test_len >= pat_len,
234+
// and if $actual_len > test_len. We can't advance.
235+
fully_matched = false;
236+
None
237+
}
238+
(Ordering::Greater, false) => {
239+
// test_len != pat_len, so if $actual_len = test_len, then
240+
// $actual_len != pat_len.
241+
fully_matched = false;
242+
Some(TestBranch::Failure)
243+
}
244+
}
245+
}
246+
(
247+
&TestKind::Len { len: test_len, op: BinOp::Ge },
248+
&TestCase::Slice { len, variable_length },
249+
) => {
250+
// the test is `$actual_len >= test_len`
251+
match (test_len.cmp(&(len as u64)), variable_length) {
252+
(Ordering::Equal, true) => {
253+
// $actual_len >= test_len = pat_len,
254+
// so we can match.
255+
fully_matched = true;
256+
Some(TestBranch::Success)
257+
}
258+
(Ordering::Less, _) | (Ordering::Equal, false) => {
259+
// test_len <= pat_len. If $actual_len < test_len,
260+
// then it is also < pat_len, so the test passing is
261+
// necessary (but insufficient).
262+
fully_matched = false;
263+
Some(TestBranch::Success)
264+
}
265+
(Ordering::Greater, false) => {
266+
// test_len > pat_len. If $actual_len >= test_len > pat_len,
267+
// then we know we won't have a match.
268+
fully_matched = false;
269+
Some(TestBranch::Failure)
270+
}
271+
(Ordering::Greater, true) => {
272+
// test_len < pat_len, and is therefore less
273+
// strict. This can still go both ways.
274+
fully_matched = false;
275+
None
276+
}
277+
}
278+
}
279+
280+
(TestKind::Range(test), TestCase::Range(pat)) => {
281+
if test == pat {
282+
fully_matched = true;
283+
Some(TestBranch::Success)
284+
} else {
285+
fully_matched = false;
286+
// If the testing range does not overlap with pattern range,
287+
// the pattern can be matched only if this test fails.
288+
if !test.overlaps(pat, self.tcx)? { Some(TestBranch::Failure) } else { None }
289+
}
290+
}
291+
(TestKind::Range(range), &TestCase::Constant { value }) => {
292+
fully_matched = false;
293+
if !range.contains(value, self.tcx)? {
294+
// `value` is not contained in the testing range,
295+
// so `value` can be matched only if this test fails.
296+
Some(TestBranch::Failure)
297+
} else {
298+
None
299+
}
300+
}
301+
302+
(TestKind::Eq { value: test_val, .. }, TestCase::Constant { value: case_val }) => {
303+
if test_val == case_val {
304+
fully_matched = true;
305+
Some(TestBranch::Success)
306+
} else {
307+
fully_matched = false;
308+
Some(TestBranch::Failure)
309+
}
310+
}
311+
312+
(TestKind::Deref { temp: test_temp, .. }, TestCase::Deref { temp, .. })
313+
if test_temp == temp =>
314+
{
315+
fully_matched = true;
316+
Some(TestBranch::Success)
317+
}
318+
319+
(TestKind::Never, _) => {
320+
fully_matched = true;
321+
Some(TestBranch::Success)
322+
}
323+
324+
(
325+
TestKind::Switch { .. }
326+
| TestKind::SwitchInt { .. }
327+
| TestKind::If
328+
| TestKind::Len { .. }
329+
| TestKind::Range { .. }
330+
| TestKind::Eq { .. }
331+
| TestKind::Deref { .. },
332+
_,
333+
) => {
334+
fully_matched = false;
335+
None
336+
}
337+
};
338+
339+
if fully_matched {
340+
// Replace the match pair by its sub-pairs.
341+
let match_pair = candidate.match_pairs.remove(match_pair_index);
342+
candidate.match_pairs.extend(match_pair.subpairs);
343+
// Move or-patterns to the end.
344+
candidate.sort_match_pairs();
345+
}
346+
347+
ret
348+
}
349+
}

0 commit comments

Comments
 (0)