From b196d0b0dc92305c9daebdfa77cb243bc4d1e25d Mon Sep 17 00:00:00 2001 From: AsthaMishra Date: Tue, 25 Aug 2026 20:37:08 +0000 Subject: [PATCH 1/3] keep the trailing semicolon on loops that break with a value --- src/utils.rs | 33 ++++++++++++++++++++++++++++++--- tests/source/issue-7061.rs | 7 +++++++ tests/target/issue-7061.rs | 9 +++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tests/source/issue-7061.rs create mode 100644 tests/target/issue-7061.rs diff --git a/src/utils.rs b/src/utils.rs index 15a4fce9348..27a43ae4dd6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,6 +5,7 @@ use rustc_ast::ast::{ self, Attribute, ImplRestriction, MetaItem, MetaItemInner, MetaItemKind, MutRestriction, NodeId, Path, RestrictionKind, Visibility, VisibilityKind, }; +use rustc_ast::visit; use rustc_ast_pretty::pprust; use rustc_span::{BytePos, LocalExpnId, Span, Symbol, SyntaxContext, sym, symbol}; use unicode_width::UnicodeWidthStr; @@ -344,6 +345,33 @@ pub(crate) fn semicolon_for_expr(context: &RewriteContext<'_>, expr: &ast::Expr) } } +/// Does this `loop` evaluate to something other than `()`? +/// +/// `loop { break v }` evaluates to `v`, so the trailing semicolon is what +/// discards that value. Removing it leaves a bare expression statement, +/// which rustc requires to be `()` +fn loop_breaks_with_value(expr: &ast::Expr) -> bool { + struct BreakWithValue(bool); + + impl<'ast> visit::Visitor<'ast> for BreakWithValue { + fn visit_expr(&mut self, ex: &'ast ast::Expr) { + match ex.kind { + ast::ExprKind::Break(_, Some(_)) => self.0 = true, + _ => { + visit::walk_expr(self, ex); + } + } + } + } + + let ast::ExprKind::Loop(ref block, ..) = expr.kind else { + return false; + }; + let mut finder = BreakWithValue(false); + visit::walk_block(&mut finder, block); + finder.0 +} + #[inline] pub(crate) fn semicolon_for_stmt( context: &RewriteContext<'_>, @@ -352,14 +380,13 @@ pub(crate) fn semicolon_for_stmt( ) -> bool { match stmt.kind { ast::StmtKind::Semi(ref expr) => match expr.kind { - ast::ExprKind::While(..) | ast::ExprKind::Loop(..) | ast::ExprKind::ForLoop { .. } => { - false - } + ast::ExprKind::While(..) | ast::ExprKind::ForLoop { .. } => false, ast::ExprKind::Break(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Ret(..) => { // The only time we can skip the semi-colon is if the config option is set to false // **and** this is the last expr (even though any following exprs are unreachable) context.config.trailing_semicolon() || !is_last_expr } + ast::ExprKind::Loop(..) => loop_breaks_with_value(expr), _ => true, }, ast::StmtKind::Expr(..) => false, diff --git a/tests/source/issue-7061.rs b/tests/source/issue-7061.rs new file mode 100644 index 00000000000..6e790be8776 --- /dev/null +++ b/tests/source/issue-7061.rs @@ -0,0 +1,7 @@ +unsafe fn foo() -> i32 { 42 } + +fn main () { + 'label: loop { + break 'label unsafe { foo() } + }; +} \ No newline at end of file diff --git a/tests/target/issue-7061.rs b/tests/target/issue-7061.rs new file mode 100644 index 00000000000..e85be409e4e --- /dev/null +++ b/tests/target/issue-7061.rs @@ -0,0 +1,9 @@ +unsafe fn foo() -> i32 { + 42 +} + +fn main() { + 'label: loop { + break 'label unsafe { foo() }; + }; +} From fd45ddd95a8dedd8ea04acb4ed366d8cc1c3f0c7 Mon Sep 17 00:00:00 2001 From: AsthaMishra Date: Tue, 25 Aug 2026 21:22:11 +0000 Subject: [PATCH 2/3] more test cases added --- tests/source/issue-7061.rs | 13 +++++++++++++ tests/target/issue-7061.rs | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tests/source/issue-7061.rs b/tests/source/issue-7061.rs index 6e790be8776..44344463f1a 100644 --- a/tests/source/issue-7061.rs +++ b/tests/source/issue-7061.rs @@ -4,4 +4,17 @@ fn main () { 'label: loop { break 'label unsafe { foo() } }; +} + +// A `loop` that cannot produce a value keeps the old behaviour +// redundant semicolon is removed. +fn no_value() { + loop { break }; + while false { }; + for _ in 0..0 { }; +} + +//minimal loop that produces a value keeps the semicolon +fn minimal() { + loop { break 5 }; } \ No newline at end of file diff --git a/tests/target/issue-7061.rs b/tests/target/issue-7061.rs index e85be409e4e..f1f5e5eec1d 100644 --- a/tests/target/issue-7061.rs +++ b/tests/target/issue-7061.rs @@ -7,3 +7,20 @@ fn main() { break 'label unsafe { foo() }; }; } + +// A `loop` that cannot produce a value keeps the old behaviour +// redundant semicolon is removed. +fn no_value() { + loop { + break; + } + while false {} + for _ in 0..0 {} +} + +//minimal loop that produces a value keeps the semicolon +fn minimal() { + loop { + break 5; + }; +} From 9086e26d0d818d1a8e5aee9b2718f5657d3edfaf Mon Sep 17 00:00:00 2001 From: AsthaMishra Date: Thu, 27 Aug 2026 16:34:10 +0000 Subject: [PATCH 3/3] nested loop test cases added --- tests/source/issue-7061.rs | 57 ++++++++++++++++++++++++++++++++++++-- tests/target/issue-7061.rs | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/tests/source/issue-7061.rs b/tests/source/issue-7061.rs index 44344463f1a..b255bbb3eff 100644 --- a/tests/source/issue-7061.rs +++ b/tests/source/issue-7061.rs @@ -6,6 +6,12 @@ fn main () { }; } +fn test_for_issue_5377() { + loop { + break false + }; +} + // A `loop` that cannot produce a value keeps the old behaviour // redundant semicolon is removed. fn no_value() { @@ -15,6 +21,53 @@ fn no_value() { } //minimal loop that produces a value keeps the semicolon -fn minimal() { +fn minimal(){ loop { break 5 }; -} \ No newline at end of file +} + +//nested loop tests +fn nested_inner_break() { + loop { + loop { break 5 }; + break + }; +} + +fn nested_labeled_break() { + 'outer: loop { + loop { + break 'outer 5 + } + }; +} + +fn nested_deeply() { + 'outer: loop { + loop { + loop { + break 'outer unsafe { foo() } + } + } + }; +} + +fn nested_deeply_four_layers() { + 'outer: loop { + loop { + loop { + loop { + break 'outer unsafe { foo() } + } + } + } + }; +} + + +fn break_inside_closure() { + loop { + let f = || loop { break 5 }; + let _ = f(); + break + }; +} diff --git a/tests/target/issue-7061.rs b/tests/target/issue-7061.rs index f1f5e5eec1d..994388862a2 100644 --- a/tests/target/issue-7061.rs +++ b/tests/target/issue-7061.rs @@ -8,6 +8,12 @@ fn main() { }; } +fn test_for_issue_5377() { + loop { + break false; + }; +} + // A `loop` that cannot produce a value keeps the old behaviour // redundant semicolon is removed. fn no_value() { @@ -24,3 +30,53 @@ fn minimal() { break 5; }; } + +//nested loop tests +fn nested_inner_break() { + loop { + loop { + break 5; + }; + break; + }; +} + +fn nested_labeled_break() { + 'outer: loop { + loop { + break 'outer 5; + } + }; +} + +fn nested_deeply() { + 'outer: loop { + loop { + loop { + break 'outer unsafe { foo() }; + } + } + }; +} + +fn nested_deeply_four_layers() { + 'outer: loop { + loop { + loop { + loop { + break 'outer unsafe { foo() }; + } + } + } + }; +} + +fn break_inside_closure() { + loop { + let f = || loop { + break 5; + }; + let _ = f(); + break; + }; +}