Skip to content

Commit 2de2456

Browse files
committed
Auto merge of #143376 - dianne:guard-scope, r=matthewjasper
add a scope for `if let` guard temporaries and bindings This fixes my concern with `if let` guard drop order, namely that the guard's bindings and temporaries were being dropped after their arm's pattern's bindings, instead of before (#141295 (comment)). The guard's bindings and temporaries now live in a new scope, which extends until (but not past) the end of the arm, guaranteeing they're dropped before the arm's pattern's bindings. This only introduces a new scope for match arms with guards. Perf results (#143376 (comment)) seemed to indicate there wasn't a significant hit to introduce a new scope on all match arms, but guard patterns (#129967) will likely benefit from only adding new scopes when necessary (with some patterns requiring multiple nested scopes). Tracking issue for `if_let_guard`: #51114 Tests are adapted from examples by `@traviscross,` `@est31,` and myself on #141295.
2 parents 4c7749e + 0bdaef5 commit 2de2456

File tree

6 files changed

+172
-46
lines changed

6 files changed

+172
-46
lines changed

compiler/rustc_hir_analysis/src/check/region.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ fn resolve_arm<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, arm: &'tcx hir:
199199

200200
resolve_pat(visitor, arm.pat);
201201
if let Some(guard) = arm.guard {
202+
// We introduce a new scope to contain bindings and temporaries from `if let` guards, to
203+
// ensure they're dropped before the arm's pattern's bindings. This extends to the end of
204+
// the arm body and is the scope of its locals as well.
205+
visitor.enter_scope(Scope { local_id: arm.hir_id.local_id, data: ScopeData::MatchGuard });
206+
visitor.cx.var_parent = visitor.cx.parent;
202207
resolve_cond(visitor, guard);
203208
}
204209
resolve_expr(visitor, arm.body, false);

compiler/rustc_middle/src/middle/region.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ impl fmt::Debug for Scope {
9696
ScopeData::Destruction => write!(fmt, "Destruction({:?})", self.local_id),
9797
ScopeData::IfThen => write!(fmt, "IfThen({:?})", self.local_id),
9898
ScopeData::IfThenRescope => write!(fmt, "IfThen[edition2024]({:?})", self.local_id),
99+
ScopeData::MatchGuard => write!(fmt, "MatchGuard({:?})", self.local_id),
99100
ScopeData::Remainder(fsi) => write!(
100101
fmt,
101102
"Remainder {{ block: {:?}, first_statement_index: {}}}",
@@ -131,6 +132,11 @@ pub enum ScopeData {
131132
/// whose lifetimes do not cross beyond this scope.
132133
IfThenRescope,
133134

135+
/// Scope of the condition and body of a match arm with a guard
136+
/// Used for variables introduced in an if-let guard,
137+
/// whose lifetimes do not cross beyond this scope.
138+
MatchGuard,
139+
134140
/// Scope following a `let id = expr;` binding in a block.
135141
Remainder(FirstStatementIndex),
136142
}

compiler/rustc_middle/src/ty/rvalue_scopes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl RvalueScopes {
4444
debug!("temporary_scope({expr_id:?}) = {id:?} [enclosing]");
4545
return (Some(id), backwards_incompatible);
4646
}
47-
ScopeData::IfThenRescope => {
47+
ScopeData::IfThenRescope | ScopeData::MatchGuard => {
4848
debug!("temporary_scope({expr_id:?}) = {p:?} [enclosing]");
4949
return (Some(p), backwards_incompatible);
5050
}

compiler/rustc_mir_build/src/builder/matches/mod.rs

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -428,47 +428,53 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
428428
let arm_source_info = self.source_info(arm.span);
429429
let arm_scope = (arm.scope, arm_source_info);
430430
let match_scope = self.local_scope();
431+
let guard_scope = arm
432+
.guard
433+
.map(|_| region::Scope { data: region::ScopeData::MatchGuard, ..arm.scope });
431434
self.in_scope(arm_scope, arm.lint_level, |this| {
432-
let old_dedup_scope =
433-
mem::replace(&mut this.fixed_temps_scope, Some(arm.scope));
434-
435-
// `try_to_place` may fail if it is unable to resolve the given
436-
// `PlaceBuilder` inside a closure. In this case, we don't want to include
437-
// a scrutinee place. `scrutinee_place_builder` will fail to be resolved
438-
// if the only match arm is a wildcard (`_`).
439-
// Example:
440-
// ```
441-
// let foo = (0, 1);
442-
// let c = || {
443-
// match foo { _ => () };
444-
// };
445-
// ```
446-
let scrutinee_place = scrutinee_place_builder.try_to_place(this);
447-
let opt_scrutinee_place =
448-
scrutinee_place.as_ref().map(|place| (Some(place), scrutinee_span));
449-
let scope = this.declare_bindings(
450-
None,
451-
arm.span,
452-
&arm.pattern,
453-
arm.guard,
454-
opt_scrutinee_place,
455-
);
435+
this.opt_in_scope(guard_scope.map(|scope| (scope, arm_source_info)), |this| {
436+
// `if let` guard temps needing deduplicating will be in the guard scope.
437+
let old_dedup_scope =
438+
mem::replace(&mut this.fixed_temps_scope, guard_scope);
439+
440+
// `try_to_place` may fail if it is unable to resolve the given
441+
// `PlaceBuilder` inside a closure. In this case, we don't want to include
442+
// a scrutinee place. `scrutinee_place_builder` will fail to be resolved
443+
// if the only match arm is a wildcard (`_`).
444+
// Example:
445+
// ```
446+
// let foo = (0, 1);
447+
// let c = || {
448+
// match foo { _ => () };
449+
// };
450+
// ```
451+
let scrutinee_place = scrutinee_place_builder.try_to_place(this);
452+
let opt_scrutinee_place =
453+
scrutinee_place.as_ref().map(|place| (Some(place), scrutinee_span));
454+
let scope = this.declare_bindings(
455+
None,
456+
arm.span,
457+
&arm.pattern,
458+
arm.guard,
459+
opt_scrutinee_place,
460+
);
456461

457-
let arm_block = this.bind_pattern(
458-
outer_source_info,
459-
branch,
460-
&built_match_tree.fake_borrow_temps,
461-
scrutinee_span,
462-
Some((arm, match_scope)),
463-
);
462+
let arm_block = this.bind_pattern(
463+
outer_source_info,
464+
branch,
465+
&built_match_tree.fake_borrow_temps,
466+
scrutinee_span,
467+
Some((arm, match_scope)),
468+
);
464469

465-
this.fixed_temps_scope = old_dedup_scope;
470+
this.fixed_temps_scope = old_dedup_scope;
466471

467-
if let Some(source_scope) = scope {
468-
this.source_scope = source_scope;
469-
}
472+
if let Some(source_scope) = scope {
473+
this.source_scope = source_scope;
474+
}
470475

471-
this.expr_into_dest(destination, arm_block, arm.body)
476+
this.expr_into_dest(destination, arm_block, arm.body)
477+
})
472478
})
473479
.into_block()
474480
})
@@ -2517,7 +2523,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
25172523
// bindings and temporaries created for and by the guard. As a result, the drop order
25182524
// for the arm will correspond to the binding order of the final sub-branch lowered.
25192525
if matches!(schedule_drops, ScheduleDrops::No) {
2520-
self.clear_top_scope(arm.scope);
2526+
self.clear_match_arm_and_guard_scopes(arm.scope);
25212527
}
25222528

25232529
let source_info = self.source_info(guard_span);

compiler/rustc_mir_build/src/builder/scope.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,20 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
697697
block.and(rv)
698698
}
699699

700+
/// Convenience wrapper that executes `f` either within the current scope or a new scope.
701+
/// Used for pattern matching, which introduces an additional scope for patterns with guards.
702+
pub(crate) fn opt_in_scope<R>(
703+
&mut self,
704+
opt_region_scope: Option<(region::Scope, SourceInfo)>,
705+
f: impl FnOnce(&mut Builder<'a, 'tcx>) -> BlockAnd<R>,
706+
) -> BlockAnd<R> {
707+
if let Some(region_scope) = opt_region_scope {
708+
self.in_scope(region_scope, LintLevel::Inherited, f)
709+
} else {
710+
f(self)
711+
}
712+
}
713+
700714
/// Push a scope onto the stack. You can then build code in this
701715
/// scope and call `pop_scope` afterwards. Note that these two
702716
/// calls must be paired; using `in_scope` as a convenience
@@ -1750,17 +1764,24 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
17501764
success_block
17511765
}
17521766

1753-
/// Unschedules any drops in the top scope.
1767+
/// Unschedules any drops in the top two scopes.
17541768
///
1755-
/// This is only needed for `match` arm scopes, because they have one
1756-
/// entrance per pattern, but only one exit.
1757-
pub(crate) fn clear_top_scope(&mut self, region_scope: region::Scope) {
1758-
let top_scope = self.scopes.scopes.last_mut().unwrap();
1769+
/// This is only needed for pattern-matches combining guards and or-patterns: or-patterns lead
1770+
/// to guards being lowered multiple times before lowering the arm body, so we unschedle drops
1771+
/// for guards' temporaries and bindings between lowering each instance of an match arm's guard.
1772+
pub(crate) fn clear_match_arm_and_guard_scopes(&mut self, region_scope: region::Scope) {
1773+
let [.., arm_scope, guard_scope] = &mut *self.scopes.scopes else {
1774+
bug!("matches with guards should introduce separate scopes for the pattern and guard");
1775+
};
17591776

1760-
assert_eq!(top_scope.region_scope, region_scope);
1777+
assert_eq!(arm_scope.region_scope, region_scope);
1778+
assert_eq!(guard_scope.region_scope.data, region::ScopeData::MatchGuard);
1779+
assert_eq!(guard_scope.region_scope.local_id, region_scope.local_id);
17611780

1762-
top_scope.drops.clear();
1763-
top_scope.invalidate_cache();
1781+
arm_scope.drops.clear();
1782+
arm_scope.invalidate_cache();
1783+
guard_scope.drops.clear();
1784+
guard_scope.invalidate_cache();
17641785
}
17651786
}
17661787

tests/ui/drop/if-let-guards.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//! Tests drop order for `if let` guard bindings and temporaries. This is for behavior specific to
2+
//! `match` expressions, whereas `tests/ui/drop/drop-order-comparisons.rs` compares `let` chains in
3+
//! guards to `let` chains in `if` expressions. Drop order for `let` chains in guards shouldn't
4+
//! differ between Editions, so we test on both 2021 and 2024, expecting the same results.
5+
//@ revisions: e2021 e2024
6+
//@ [e2021] edition: 2021
7+
//@ [e2024] edition: 2024
8+
//@ run-pass
9+
10+
#![feature(if_let_guard)]
11+
#![deny(rust_2024_compatibility)]
12+
13+
use core::{cell::RefCell, ops::Drop};
14+
15+
fn main() {
16+
// Test that `let` guard bindings and temps are dropped before the arm's pattern's bindings.
17+
assert_drop_order(1..=6, |o| {
18+
// We move out of the scrutinee, so the drop order of the array's elements are based on
19+
// binding declaration order, and they're dropped in the arm's scope.
20+
match [o.log(6), o.log(5)] {
21+
// Partially move from the guard temporary to test drops both for temps and the binding.
22+
[_x, _y] if let [_, _z, _] = [o.log(4), o.log(2), o.log(3)]
23+
&& true => { let _a = o.log(1); }
24+
_ => unreachable!(),
25+
}
26+
});
27+
28+
// Sanity check: we don't move out of the match scrutinee when the guard fails.
29+
assert_drop_order(1..=4, |o| {
30+
// The scrutinee uses the drop order for arrays since its elements aren't moved.
31+
match [o.log(3), o.log(4)] {
32+
[_x, _y] if let _z = o.log(1)
33+
&& false => unreachable!(),
34+
_ => { let _a = o.log(2); }
35+
}
36+
});
37+
38+
// Test `let` guards' temporaries are dropped immediately when a guard fails, even if the guard
39+
// is lowered and run multiple times on the same arm due to or-patterns.
40+
assert_drop_order(1..=8, |o| {
41+
let mut _x = 1;
42+
// The match's scrutinee isn't bound by-move, so it outlives the match.
43+
match o.log(8) {
44+
// Failing a guard breaks out of the arm's scope, dropping the `let` guard's scrutinee.
45+
_ | _ | _ if let _ = o.log(_x)
46+
&& { _x += 1; false } => unreachable!(),
47+
// The temporaries from a failed guard are dropped before testing the next guard.
48+
_ if let _ = o.log(5)
49+
&& { o.push(4); false } => unreachable!(),
50+
// If the guard succeeds, we stay in the arm's scope to execute its body.
51+
_ if let _ = o.log(7)
52+
&& true => { o.log(6); }
53+
_ => unreachable!(),
54+
}
55+
});
56+
}
57+
58+
// # Test scaffolding...
59+
60+
struct DropOrder(RefCell<Vec<u64>>);
61+
struct LogDrop<'o>(&'o DropOrder, u64);
62+
63+
impl DropOrder {
64+
fn log(&self, n: u64) -> LogDrop<'_> {
65+
LogDrop(self, n)
66+
}
67+
fn push(&self, n: u64) {
68+
self.0.borrow_mut().push(n);
69+
}
70+
}
71+
72+
impl<'o> Drop for LogDrop<'o> {
73+
fn drop(&mut self) {
74+
self.0.push(self.1);
75+
}
76+
}
77+
78+
#[track_caller]
79+
fn assert_drop_order(
80+
ex: impl IntoIterator<Item = u64>,
81+
f: impl Fn(&DropOrder),
82+
) {
83+
let order = DropOrder(RefCell::new(Vec::new()));
84+
f(&order);
85+
let order = order.0.into_inner();
86+
let expected: Vec<u64> = ex.into_iter().collect();
87+
assert_eq!(order, expected);
88+
}

0 commit comments

Comments
 (0)