Hi all!
In this blog post, I'm going to talk about a feature I've been working on stabilizing
The original tracking issue was created more than 7 years ago
I've been using Rust for less than 7 years, so I’ll relay what I’ve heard: the feature was stabilized at first, but some drop-order bugs appeared, and it was quickly destabilized. I guess it was then abandoned for a long time, currently, all the drop order bugs have been fixed, and it works correctly
I encountered some code where I tried to use if let guard and then compiler says
hey, that's not stable, what are you trying to do here?
and I was like
eeh, what do you mean not stable, this is so obvious and... simple, like, how is this different from an
if guard
After that, I started investigating this feature, this was at the very beginning of my contributing journey somewhere around end of April
And I got a lot of help from another Rust team member est31, who, not long before I started looking into this, had finished stabilizing let chains which was a major stabilization for Rust and one of the most requested features overall
As you can tell from the post's title, it's about the if let guard feature, which I think is a very pretty one :3
What is this feature even about?
This is a pretty straightforward feature at first sight, I mean very straightforward, it's basically allows you to use let patterns in match guards, like this
let x = "42";
let y = Some(2);
match x.parse::<i32>() {
Ok(s) if let Some(y) = y => (),
Ok(s) => (),
_ => (),
}
This simple snippet shows what the feature does - nothing complicated, right? So roughly without this feature (there are many ways to rewrite it in Rust, I just pick one that is closest to the code above and what I'd use)
let x = "42";
let y = Some(2);
match x.parse::<i32>() {
Ok(s) => {
if let Some(y) = y {
// first branch
} else {
// second branch
}
}
_ => (),
}
Personally I don't like this, I never liked this, just because there is cases where you really don't want to create a block in match arm and just return value
struct A {
x: i32,
y: i32,
related_points: Option<Vec<A>>,
}
fn main() {
let a = A {
x: 2,
y: 3,
related_points: Some(vec![]),
};
let x = Some(42);
match x {
Some(x)
if let A { related_points, .. } = a
&& let Some(points) = related_points => points.len()
_ => 1,
};
}
So, currently you are forced to write a block like this
match x {
Some(x) => {
if let A { related_points, .. } = a
&& let Some(points) = related_points
{
points.len()
} else {
1
}
}
_ => 1,
}
As I said before, I don't like this, like, completely
As you might have already seen, if let guard could use let chains...
Let chains
Yes, you get it right, this feature is fully compatible with let chains
let chains are actually one of my favorite Rust features, even though I started learning Rust only few years ago, I can definitely tell that let chains were my most used feature since then, I do really love patterns and hope you too!
So back to let chains in if let guard, you might be wondering, what's so special about them?
Here’s the answer: let chains were originally restricted to Rust 2024+ editions, so using them in edition 2021 (or earlier) would give an error
error: let chains are only allowed in Rust 2024 or later
--> src/main.rs:4:8
|
4 | if let Some(x) = Some(42) && x.is_positive() {
|
This will not work as we just tried above, but you already have seen the possibility of using let chains in if let guard
A few questions might come to mind:
- Does that mean that
if let guardgoing to be stabilized in edition 2024+? - Or does it mean that I could use
if let guardin all editions, but usinglet chainsinif let guardwill be restricted
Sooo... my answer will be pretty short and direct: using let chains in if let guard will be allowed in all editions, which means
fn main() {
match 0 {
_ if let Some(x) = Some(42) && x.is_negative() => println!("wow!"),
}
}
This code will work perfectly fine in all editions even though it contains let chains
Important note here:
Currently, if you try to run this code in edition 2021 or lower, it will give you an error, about let chains, just because, when we are parsing if let guard in compiler we do have a check for let chains to gave such error,
if has_let_expr(&cond) {
let span = if_span.to(cond.span);
self.psess.gated_spans.gate(sym::if_let_guard, span);
}
But don't worry, this will be deleted with stabilization, it's fine that it doesn't work now, it will work as stabilized
This is snippet from my code that I wrote to fix a diagnostic problem using both of these features, we are using a lot of unstable features in compiler so which is why I was able to do this,
Btw, if you aren't comfortable with understanding this code, which is 100% fine, I'm not waiting for the reader to be a rustc developer, just try to see the differences in form itself, the semantics are pretty much the same
The problem was that an Op::AssignOp(assign_op) branch already existed and extending it for this new check would have been messy, since it wasn't just a few lines, so it would have dramatically bloated the original branch, the solution was to create a separate branch and do all the checks right in the match guard:
Op::AssignOp(assign_op)
if assign_op.node == hir::AssignOpKind::AddAssign
&& let hir::ExprKind::Binary(bin_op, left, right) = &lhs_expr.kind
&& bin_op.node == hir::BinOpKind::And
&& crate::op::contains_let_in_chain(left)
&& let hir::ExprKind::Path(hir::QPath::Resolved(_, path)) =
&right.kind
&& matches!(path.res, hir::def::Res::Local(_)) =>
{
// give a nice user friendly error here :3
}
Actually, for those who wonder how the same code will looks without all this stuff is... this...
Op::AssignOp(assign_op) => {
if assign_op.node == hir::AssignOpKind::AddAssign {
if let hir::ExprKind::Binary(bin_op, left, right) = &lhs_expr.kind {
if bin_op.node == hir::BinOpKind::And {
if crate::op::contains_let_in_chain(left) {
if let hir::ExprKind::Path(hir::QPath::Resolved(_, path)) =
&right.kind
{
if matches!(path.res, hir::def::Res::Local(_)) {
// give an nice user friendly error here :3
}
}
}
}
}
}
}
This is far from perfection! So please use both of these nice features to get closer to it ^^,
When this feature will be out?
Actually, in perfect scenario, I'd say that this feature will be stable at 1.95, which is very optimistic. There is only one blocker left: documentation, so as far as I can tell, it will be FCP'ed and merged after the documentation is merged, in less optimistic scenario I think it will be ready to 1.96, but, to be honest both of these scenarios are very close and mostly important is very realistic, because all the concerns about drop order have been resolved, there is nothing blocking it
FAQ section
This section will answer some of the questions that might come up in your head
Why are let chains in if let guard stable on all editions?
That's a really good question! It's hard to say with complete confidence since I wasn't working on the original let chains stabilization and I'm not deeply familiar with that part of the compiler. But from what I've learned from the team: let chains in if let guard don't suffer from the drop order issues that affect regular if chains because there's no else block like in if let has that can cause scoping complications.
To stabilize let chains for Edition 2024, the if_let_rescope feature was introduced to fix drop order behavior in contexts with else blocks. Match guards never had this problem—they're just boolean conditions without an else branch, so temporaries already had predictable scoping. Since if let guard already behaves correctly without needing the rescoping fix, it works consistently across all editions.
What was the drop order bug that was fixed?
Well, I can't tell about previous stabilization, since not much is known about it, but I can speak about current one
So in current stabilization, right after it was FCP'ed and I had fixed all (30+) the last nits, and it was sitting in the queue to be merged, dianne raised a very good point with drop order,
I will leave her comment here, just in case someone wants to see it for themselves, but I will explain this a little, honestly this comment is pretty well written as it, but here's the problem:
match SignificantDrop(0) {
x if let y = SignificantDrop(&x) => {}
_ => unreachable!(),
}
x drops before y which is not obvious and maybe incorrect sometimes, and another case shows the actual problem of inconsistent drop order pretty well
match [LogDrop(o, 2), LogDrop(o, 1)] {
[x, y] if let z = LogDrop(o, 3) => {}
_ => unreachable!(),
}
First, let me explain how to read this, it exactly show drop order by numbers, so
LogDrop(o, 1)will drop first,LogDrop(o, 2)second and so on
So here, it will drop everything in the following order y -> x -> z, which is also not correct, you would expect right to left drop order, now, thanks to dianne's efforts, it works as intended and expected: z -> y -> x
match [LogDrop(o, 3), LogDrop(o, 2)] {
[x, y] if let z = LogDrop(o, 1) => {}
_ => (),
}
This behavior was fixed by dianne in this pull request
What is the current drop order behavior?
So yes, as you might have seen above, the drop order is correct and right to left order
match [LogDrop(o, 5), LogDrop(o, 4)] {
[x, y]
if let z = LogDrop(o, 3)
&& let w = LogDrop(o, 2)
&& let v = LogDrop(o, 1) => {}
_ => (),
}
This is consistent and stable, so there is nothing to worry about. There was also a bug with pin!() that was found also during the limit testing of if let guard by theemathas, so this bug looked like this and it wasn't necessarily a bug in the if let guard, but let chains one
if let x = LoudDrop("0")
&& let y = pin!(LoudDrop("1"))
&& let z = pin!(LoudDrop("2"))
&& let w = LoudDrop("3")
{}
// 3
// 0
// 2
// 1
And once again, this is not what you would expect, but yeah, on current stable version this is already fixed and working as intented
Also was fixed by dianne here
Performance question
Q: Does this compile to the same code as using a block, and/or does it have any overhead from creating a block with an if let?
Well, this is a reasonable question and concern, but answer is no, both of these code examples compile to the same assembly, have the same semantics, and the same drop order/scope behavior. So yeah, I'd say they are identical
match 0 {
_ if let Some(new_x) = Some(2) && new_x > 150 => x = new_x,
_ => x = 1,
}
match 0 {
_ => {
if let Some(new_x) = Some(2) && new_x > 150 {
x = new_x;
} else {
x = 1;
}
}
}
Shadowing
Quick quiz: what will this print?
match Some(4) {
x if let Some(x) = Some(2) && let Some(x) = Some(5) => println!("{x}"),
_ => panic!()
}
if you answered 5, you are correct, so yeah, it will just shadow all previous x's
Irrefutable Patterns
The compiler will correctly understand if a pattern is irrefutable, so like in this example
match Some(()) {
Some(x) if let () = x => {}
_ => {}
}
It will give you a nice warning :3
warning: irrefutable `if let` guard pattern
--> src/main.rs:41:20
|
41 | Some(x) if let () = x => {}
| ^^^^^^^^^^
|
= note: this pattern will always match, so the guard is useless
= help: consider removing the guard and adding a `let` inside the match arm
= note: `#[warn(irrefutable_let_patterns)]` on by default
Conclusion & Personal Note
I don't like conclusions because they usually just repeat everything you already read, so I'll keep this short: the feature is nice, the blockers are resolved, around 1.95 hopefully!
That's all! If you like what I'm doing and want to ✨ support ✨ my work, I have a sponsor page. I also just updated it with information on how you can support me without crypto (using crypto, yes xD)
Working on long-running compiler takes time and focus - which brings me to a small personal note
I'm planning to move to my partner's city soon. While I have the funds saved up, any support to make this transition smoother and less stressful would mean the world to me
With heartfelt thanks to the Rust community!