Skip to content

Allow hoisting else block stmts for while_let_loop - #16404

Open
borngraced wants to merge 1 commit into
rust-lang:masterfrom
borngraced:while-let-loop-hoist-stmts
Open

Allow hoisting else block stmts for while_let_loop#16404
borngraced wants to merge 1 commit into
rust-lang:masterfrom
borngraced:while-let-loop-hoist-stmts

Conversation

@borngraced

@borngraced borngraced commented Jan 16, 2026

Copy link
Copy Markdown
Contributor

View all comments

fixes: #16393

changelog: [while_let_loop]: Allow hoisting else block stmts

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jan 16, 2026
@rustbot

rustbot commented Jan 16, 2026

Copy link
Copy Markdown
Collaborator

r? @samueltardieu

rustbot has assigned @samueltardieu.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@borngraced

borngraced commented Jan 16, 2026

Copy link
Copy Markdown
Contributor Author

Is it okay to eliminate too many argument warnings by allowing clippy too_many_arguments or perhaps I'll need to refactor the fn particularly.. 🤔

@ada4a

ada4a commented Jan 16, 2026

Copy link
Copy Markdown
Contributor

Is it okay to eliminate too many argument warnings by allowing clippy too_many_arguments or perhaps I'll need to refactor the fn particularly.. 🤔

Yeah it's usually fine... but if you are able to come up with a refactoring, then by all means!

Comment thread tests/ui/while_let_loop.rs Outdated
Comment on lines 259 to 265
// The suggestion is:
// ```rust
// while let Some(x) = std::hint::black_box(None::<i32>) {
// println!("x = {x}");
// }
// println!("fail");
// ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be fine to remove now, as the suggestion is contained in the .stderr file

Comment on lines +221 to +222
LL ~ while let Some(x) = std::hint::black_box(None::<i32>) { .. }
LL + println!("fail");

@ada4a ada4a Jan 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One problem with this is that, since one of the statements is moved out, it's a bit unclear what exactly should the ellipses be replaced with. Not sure how to fix this, short of reintroducing body reconstruction in the suggestion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about adding a help note to explain what the user should do with the { .. }? like so:

 help: try
     |
  LL ~     while let Some(x) = y { .. }
  LL +     println!("fail");
     |
     = note: the statements above are hoisted from the `else` block; move the original loop body into the `while let`

println!("x = {x}");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to add tests for:

  • multiple statements
  • a semicolon-less statement, like if true { whatever; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}",
indent = snippet_indent(cx, expr.span).unwrap_or_default(),
)
format!("\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}",)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
format!("\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}",)
format!("\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}")

I wish rustfmt would handle this...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filed an issue rust-lang/rustfmt#6768

@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from 67edf9b to ea92abc Compare January 17, 2026 17:45
@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@rustbot rustbot added has-merge-commits PR has merge commits, merge with caution. S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Jan 23, 2026
@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from 5d7b991 to ea92abc Compare January 23, 2026 13:54
@rustbot

This comment has been minimized.

@rustbot rustbot removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) has-merge-commits PR has merge commits, merge with caution. labels Jan 23, 2026
@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from ea92abc to b4bc45c Compare January 23, 2026 19:55

@samueltardieu samueltardieu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would really be great to have autofixes, that was the goal of hoisting the statements out of the loop. We should attempt this, even though this might produce larger output. If needed, there are ways to present one view to the human, and one to tools so that they can do large transforms (tool-only suggestions).

View changes since this review

Comment on lines 34 to 98
hoistable_stmts: None,
},
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be written more concisely:

Suggested change
}
let (let_pat, let_expr, inner_expr, hoistable_stmts) = if let Some(if_let) = higher::IfLet::hir(cx, init)
&& let Some(else_expr) = if_let.if_else
&& is_simple_break_expr(else_expr)
{
(if_let.let_pat, if_let.let_expr, Some(if_let.if_then), None)
} else if els.is_some_and(is_simple_break_block)
&& let Some((pat, _)) = let_info
{
(pat, init, None, None)
} else if let Some(els_block) = els
&& let Some((pat, _)) = let_info
&& let Some(hoistable) = extract_hoistable_stmts(els_block)
{
(pat, init, None, Some(hoistable))
} else if let ExprKind::Match(scrutinee, [arm1, arm2], MatchSource::Normal) = init.kind
&& arm1.guard.is_none()
&& arm2.guard.is_none()
&& is_simple_break_expr(arm2.body)
{
(arm1.pat, scrutinee, Some(arm1.body), None)
} else {
return;
};
let while_let_info = WhileLetInfo {
let_pat,
let_expr,
has_trailing_exprs,
let_info,
inner_expr,
hoistable_stmts,
};
could_be_while_let(cx, expr, while_let_info);

I'm not even sure the WhileLetInfo structure is really useful.

Comment thread tests/ui/while_let_loop.rs Outdated
}

fn no_hoist_with_labeled_break() {
// Should NOT lint: the else block contains a labeled break to outer loop

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would that be a problem? A break to an inner loop would be hoisted fine along with the inner loop, while a break to an outer loop would be hoisted fine and, as expected, break from the outer loop. The important point here is that you have a break statement for the loop you're trying to transform at the end of the else. You don't care if the statements before the break are diverging. The only important things to check IMO is that the hoisted statement should not contain a break or a continue to the loop it getting hoisted out of, except the last statement or expression of course.

Note that you can check the target of a break by HirId (which will be the loop_expr.hir_id if loop_expr is the loop expression). You could modify is_simple_break_expr as:

/// Checks if `expr` contains a single unlabeled `break` expression or statement, possibly embedded
/// inside other blocks.
fn is_simple_break_expr(expr: &Expr<'_>, target_id: HirId) -> bool {
    matches!(peel_blocks_with_stmt(expr).kind, ExprKind::Break(dest, None) if dest.target_id == Ok(target_id))
}

(and modify is_simple_break_loop accordingly)

Or am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only breaks/continues that would be problematic are ones targeting the loop being transformed itself, because that loop's structure changes to a while let...so you're right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also like to see some tests where labels are applied on the loop being transformed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all done

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Feb 22, 2026
@rustbot

rustbot commented Feb 22, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from b4bc45c to bb8a255 Compare March 1, 2026 15:02
@rustbot

This comment has been minimized.

@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from ddd6747 to e07f19c Compare March 2, 2026 02:19
@borngraced

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Mar 16, 2026
@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from e07f19c to 83e12a7 Compare April 10, 2026 12:16
@rustbot

This comment has been minimized.

@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from 83e12a7 to aef8e9a Compare April 27, 2026 20:47
@rustbot

This comment has been minimized.

add test cases for semicolon_less and multiple stmts

extract could_be_while_let fn param into a struct

fix clippy warnings

refactor: remove WhileLetInfo struct, flatten match chain

use HirId for break/continue targeting, add label support and autofix

cargo dev fmt
@borngraced
borngraced force-pushed the while-let-loop-hoist-stmts branch from aef8e9a to 8e18229 Compare May 15, 2026 13:14
@rustbot

rustbot commented May 15, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@rustbot

rustbot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

☔ The latest upstream changes (possibly #17644) made this pull request unmergeable. Please resolve the merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties

Projects

None yet

Development

Successfully merging this pull request may close these issues.

while_let_loop: hoist code out of the loop

4 participants