Skip to content

Commit 43cc1e1

Browse files
Auto merge of #143376 - dianne:guard-scope, r=<try>
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. So far, this is the only way I've thought of to achieve this without explicitly rescheduling guards' drops to move them after the arm's pattern's. I'm not sure this should be merged as-is. It's a little hacky and it introduces a new scope for *all* match arms rather than just those with `if let` guards. However, since I'm looking for feedback on the approach, I figured this is a relatively simple way to present it. As I mention in a FIXME comment, something like this will be needed for guard patterns (#129967) too[^1], so I think the final version should maybe only add these scopes as needed. That'll be better for perf too. Tracking issue for `if_let_guard`: #51114 Tests are adapted from examples by `@traviscross,` `@est31,` and myself on #141295. cc, as I'd like your input on this. I'm not entirely sure who to request for scoping changes, but let's start with r? `@Nadrieril` since this relates to guard patterns, we talked about it recently, and rustbot's going to ping you anyway. Feel free to reassign! [^1]: e.g., new scopes are needed to keep failed guards inside `let` chain patterns from dropping existing bindings/temporaries; something like this could give a way of doing that without needing to reschedule drops. Unfortunately it may not help keep failed guards in `let` statement patterns from dropping the `let` statement's initializer, so it isn't a complete solution. I'm still not sure how to do that without rescheduling drops, changing how `let` statements' scopes work, or restricting the functionality of guard patterns in `let` statements (including `let`-`else`).
2 parents a413f77 + f501cdd commit 43cc1e1

File tree

6 files changed

+164
-58
lines changed

6 files changed

+164
-58
lines changed

compiler/rustc_hir_analysis/src/check/region.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ fn resolve_arm<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, arm: &'tcx hir:
191191
visitor.cx.var_parent = visitor.cx.parent;
192192

193193
resolve_pat(visitor, arm.pat);
194+
// In order to ensure bindings and temporaries from `if let` guards are dropped before the arm's
195+
// pattern's bindings, we introduce a new scope nested within the arm's. This contains the
196+
// guard's bindings as well as any temporaries and scopes introduced within the arm's body.
197+
// `ScopeData::IfThenRescope` is used as it restricts temporaries' lifetimes.
198+
// FIXME(guard_patterns): This will likewise be needed for guard patterns. Nesting will be more
199+
// complex in that case, and multiple may be needed for `let` guards inside `let` chains.
200+
visitor.enter_scope(Scope { local_id: arm.hir_id.local_id, data: ScopeData::IfThenRescope });
201+
visitor.cx.var_parent = visitor.cx.parent;
194202
if let Some(guard) = arm.guard {
195203
resolve_expr(visitor, guard, !has_let_expr(guard));
196204
}

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

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -448,47 +448,51 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
448448
let arm_scope = (arm.scope, arm_source_info);
449449
let match_scope = self.local_scope();
450450
self.in_scope(arm_scope, arm.lint_level, |this| {
451-
let old_dedup_scope =
452-
mem::replace(&mut this.fixed_temps_scope, Some(arm.scope));
453-
454-
// `try_to_place` may fail if it is unable to resolve the given
455-
// `PlaceBuilder` inside a closure. In this case, we don't want to include
456-
// a scrutinee place. `scrutinee_place_builder` will fail to be resolved
457-
// if the only match arm is a wildcard (`_`).
458-
// Example:
459-
// ```
460-
// let foo = (0, 1);
461-
// let c = || {
462-
// match foo { _ => () };
463-
// };
464-
// ```
465-
let scrutinee_place = scrutinee_place_builder.try_to_place(this);
466-
let opt_scrutinee_place =
467-
scrutinee_place.as_ref().map(|place| (Some(place), scrutinee_span));
468-
let scope = this.declare_bindings(
469-
None,
470-
arm.span,
471-
&arm.pattern,
472-
arm.guard,
473-
opt_scrutinee_place,
474-
);
451+
let guard_scope =
452+
region::Scope { data: region::ScopeData::IfThenRescope, ..arm.scope };
453+
this.in_scope((guard_scope, arm_source_info), LintLevel::Inherited, |this| {
454+
let old_dedup_scope =
455+
mem::replace(&mut this.fixed_temps_scope, Some(guard_scope));
456+
457+
// `try_to_place` may fail if it is unable to resolve the given
458+
// `PlaceBuilder` inside a closure. In this case, we don't want to include
459+
// a scrutinee place. `scrutinee_place_builder` will fail to be resolved
460+
// if the only match arm is a wildcard (`_`).
461+
// Example:
462+
// ```
463+
// let foo = (0, 1);
464+
// let c = || {
465+
// match foo { _ => () };
466+
// };
467+
// ```
468+
let scrutinee_place = scrutinee_place_builder.try_to_place(this);
469+
let opt_scrutinee_place =
470+
scrutinee_place.as_ref().map(|place| (Some(place), scrutinee_span));
471+
let scope = this.declare_bindings(
472+
None,
473+
arm.span,
474+
&arm.pattern,
475+
arm.guard,
476+
opt_scrutinee_place,
477+
);
475478

476-
let arm_block = this.bind_pattern(
477-
outer_source_info,
478-
branch,
479-
&built_match_tree.fake_borrow_temps,
480-
scrutinee_span,
481-
Some((arm, match_scope)),
482-
EmitStorageLive::Yes,
483-
);
479+
let arm_block = this.bind_pattern(
480+
outer_source_info,
481+
branch,
482+
&built_match_tree.fake_borrow_temps,
483+
scrutinee_span,
484+
Some((arm, match_scope)),
485+
EmitStorageLive::Yes,
486+
);
484487

485-
this.fixed_temps_scope = old_dedup_scope;
488+
this.fixed_temps_scope = old_dedup_scope;
486489

487-
if let Some(source_scope) = scope {
488-
this.source_scope = source_scope;
489-
}
490+
if let Some(source_scope) = scope {
491+
this.source_scope = source_scope;
492+
}
490493

491-
this.expr_into_dest(destination, arm_block, arm.body)
494+
this.expr_into_dest(destination, arm_block, arm.body)
495+
})
492496
})
493497
.into_block()
494498
})
@@ -569,7 +573,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
569573
// be bound for each candidate.
570574
for sub_branch in branch.sub_branches {
571575
if let Some(arm) = arm {
572-
self.clear_top_scope(arm.scope);
576+
self.clear_match_arm_scope(arm.scope);
573577
}
574578
let binding_end = self.bind_and_guard_matched_candidate(
575579
sub_branch,

compiler/rustc_mir_build/src/builder/scope.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,17 +1724,24 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
17241724
success_block
17251725
}
17261726

1727-
/// Unschedules any drops in the top scope.
1727+
/// Unschedules any drops in the top two scopes.
17281728
///
17291729
/// This is only needed for `match` arm scopes, because they have one
17301730
/// entrance per pattern, but only one exit.
1731-
pub(crate) fn clear_top_scope(&mut self, region_scope: region::Scope) {
1732-
let top_scope = self.scopes.scopes.last_mut().unwrap();
1731+
// FIXME(@dianne): This can hopefully be removed if we only lower guards once.
1732+
pub(crate) fn clear_match_arm_scope(&mut self, region_scope: region::Scope) {
1733+
let [.., arm_scope, guard_scope] = &mut *self.scopes.scopes else {
1734+
bug!("match arms should introduce two scopes");
1735+
};
17331736

1734-
assert_eq!(top_scope.region_scope, region_scope);
1737+
assert_eq!(arm_scope.region_scope, region_scope);
1738+
assert_eq!(guard_scope.region_scope.data, region::ScopeData::IfThenRescope);
1739+
assert_eq!(guard_scope.region_scope.local_id, region_scope.local_id);
17351740

1736-
top_scope.drops.clear();
1737-
top_scope.invalidate_cache();
1741+
arm_scope.drops.clear();
1742+
arm_scope.invalidate_cache();
1743+
guard_scope.drops.clear();
1744+
guard_scope.invalidate_cache();
17381745
}
17391746
}
17401747

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

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

0 commit comments

Comments
 (0)