Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Comment on lines +348 to +373

@ytmimi ytmimi Aug 25, 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.

How does rustfmt know that v doesn't resolve to ()? Does it make a difference?

For example:

fn main() {
        'a:  loop  {
                break  'a  some_callback_function_that_returns_empty_tuple()
        };


        'b:  loop  {
                break  'b  some_macro_that_evaluates_to_empty_tuple!()
        };
}

View changes since the review

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.

no rustfmt does not know what it will resolve to. No it does not make a difference as ; is kept either way, If loop breaks into a value and does not have semicolon then you get error - E0308 otherwise it is just redundant


#[inline]
pub(crate) fn semicolon_for_stmt(
context: &RewriteContext<'_>,
Expand All @@ -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,
Expand Down
73 changes: 73 additions & 0 deletions tests/source/issue-7061.rs

@ytmimi ytmimi Aug 25, 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.

What about loops deeply nested inside other loops loops?

View changes since the review

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.

yes it works in nested loops as well

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
unsafe fn foo() -> i32 { 42 }

fn main () {
'label: loop {
break 'label unsafe { foo() }
};
}

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() {
loop { break };
while false { };
for _ in 0..0 { };
}

//minimal loop that produces a value keeps the semicolon
fn minimal(){
loop { 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
};
}
82 changes: 82 additions & 0 deletions tests/target/issue-7061.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
unsafe fn foo() -> i32 {
42
}

fn main() {
'label: loop {
break 'label unsafe { foo() };
};
}

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() {
loop {
break;
}
while false {}
for _ in 0..0 {}
}

//minimal loop that produces a value keeps the semicolon
fn minimal() {
loop {
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;
};
}