Skip to content

Include closures in allow_multi_line to prevent exponential time formatting of nested closures - #7077

Closed
Manishearth wants to merge 1 commit into
rust-lang:mainfrom
Manishearth:issue-5626
Closed

Include closures in allow_multi_line to prevent exponential time formatting of nested closures#7077
Manishearth wants to merge 1 commit into
rust-lang:mainfrom
Manishearth:issue-5626

Conversation

@Manishearth

Copy link
Copy Markdown
Member

Fixes #5626

This code does some backtracking: rustfmt tries to format a closure, and if it ends up needing multiple lines, for expressions that don't come with builtin braces it adds braces and tries to format it again.

"for expressions that don't come with builtin braces" is checked by allow_multiline, and contains things like match {} etc.

It does not contain closures themselves. It should: a closure already follows the rule of "has braces if multiline", so in this case (code is multiline) formatted closures can be counted as having builtin braces.

Without this, the behavior is O(2^N): for N nested closures, each closure attempts to format the child closure as a brace-less expression-closure until it bottoms out. The innermost closure will format with a newline, which then causes each parent formatted closure to discard since the newline is treated as "format as a braced block-closure instead" signal. And then it tries to format the outermost closure as a block, but then you have the same problem with the N - 1 subclosures. Allowing closures to skip

An LLM was used to investigate this issue. Issue body and code is mine (the test is a straight copy from the issue).

Breakages

The current implementation does introduce a formatting change, unfortunately.

let f = |x| |y| {
    let z = x + y;
    z * 2
};

would previously format as

let f = |x| {
    |y| {
        let z = x + y;
        z * 2
    }
};

but now it stays unchanged. If we want to retain the original behavior, then I think we just need to skip the expression code when we have nested closures. That would be more complicated.

We could also use a style edition gate here. I wanted to open a PR first before trying to discuss how to handle the breakage.

…atting of nested closures

Fixes rust-lang#5626

This code does some backtracking: rustfmt tries to format a closure, and if it ends up needing multiple lines, for expressions that don't come with builtin braces it adds braces and tries to format it again.

"for expressions that don't come with builtin braces" is checked by allow_multiline, and contains things like `match {}` etc.

It does not contain closures themselves. It should: a closure already follows the rule of "has braces if multiline", so in *this* case (code is multiline) formatted closures can be counted as having builtin braces.

Without this, the behavior is O(2^N): for N nested closures, each closure attempts to format the child closure as a brace-less expression-closure until it bottoms out. The innermost closure will format with a newline, which then causes each parent formatted closure to discard since the newline is treated as "format as a braced block-closure instead" signal. And then it tries to format the outermost closure as a block, but then you have the same problem with the N - 1 subclosures. Allowing closures to skip

An LLM was used to investigate this issue. Issue body and code is mine (the test is a straight copy from the issue).
@rustbot rustbot added the S-waiting-on-review Status: awaiting review from the assignee but also interested parties. label Aug 27, 2026
@ytmimi

ytmimi commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

With this change will rustfmt try to format

let f = |x| {
    |y| {
        let z = x + y;
        z * 2
    }
};

back into this?

let f = |x| |y| {
    let z = x + y;
    z * 2
};

Also, this example reminded me of #5685

@Manishearth

Copy link
Copy Markdown
Member Author

@ytmimi yes

Comment on lines +11 to +27
pub fn g() {
|| || || || || || || {
H || || {
isize
|| || || || || || || {
(|| || || {
isize
|| || || || || || || || {
isize || || || || || || || || || || || || || isize!(f(), 2)
}
})()
}
}
};
isize!(::f(), 1);
}
}

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

It's tough to tell, but what is H in this context? It just looks like it's floating there.

View changes since the review

@Manishearth Manishearth Aug 27, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I need to fix the test, I copied it but I might have made a mistake when playing around with it.

@ytmimi

ytmimi commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

As was brought up on #5685 we should probably consult @rust-lang/style if this change is going to impact closure formatting moving forward. Alternatively, we might want to figure out how we can specifically target this pathological case without impacting the "regular case".

@Manishearth

Copy link
Copy Markdown
Member Author

Yeah, if this is a thing where people have actual preferences then we should not fix it this way. I may or may not poke around at doing this in a way that preserves the actual formatting.

@rustbot rustbot removed the S-waiting-on-review Status: awaiting review from the assignee but also interested parties. label Aug 27, 2026
@Manishearth Manishearth mentioned this pull request Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rustfmt hangs

3 participants