Skip to content

Don't list escaping bound regions in nested for<...> binders of E0308 notes - #159232

Merged
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
Rani367:fix-nested-hrtb-binder-printing
Aug 26, 2026
Merged

Don't list escaping bound regions in nested for<...> binders of E0308 notes#159232
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
Rani367:fix-nested-hrtb-binder-printing

Conversation

@Rani367

@Rani367 Rani367 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

The expected/found notes of "one type is more general than the other" errors could print types that are not even valid syntax, repeating lifetimes from an outer binder inside nested for<...> lists:

= note: expected mutable reference `&mut for<'a> fn(for<'a> fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`

The expected type here is actually &mut for<'a> fn(fn(&'a ())), the inner fn pointer binds nothing. In the example from #111365 the note printed for<'o> fn(for<'a, 'o> fn(&'a (), &'o ())) even though the inner binder only binds 'a.

The cause is in how cmp_fn_sig builds its for<...> prefixes. It calls name_all_regions and joins every region of the returned map into the list. But RegionFolder folded every region at or above the depth of the binder being printed (db >= self.current_index), so regions that are bound by an enclosing binder and merely escape through the current one also ended up in the map. The pretty printer's own text output already skipped those regions via a special case in the naming closure (which is why the labels of these diagnostics were correct, see #102392), but the returned map kept them, and cmp_fn_sig recurses through nested fn pointers one binder at a time, so every nested for<...> list picked up all the outer lifetimes. That special case also rebound the escaping region to self.current_index, i.e. shifted its De Bruijn index down so that it looked like it was bound by the current binder.

This PR makes RegionFolder only fold regions bound by the binder being named (db == self.current_index). Escaping regions are left untouched: they keep their index, and they already carry a name because the enclosing binder named them when it was folded (cmp_fn_sig recurses on the folded value). This makes the #102392 special case in the naming closure dead, so it is removed along with the two DebruijnIndex parameters it needed.

With this change, the two examples above print:

= note: expected mutable reference `&mut for<'a> fn(fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
= note: expected fn pointer `for<'o> fn(for<'a> fn(&'a (), &'o ()))`
           found fn pointer `fn(for<'a> fn(&'a (), &'a ()))`

The found line of tests/ui/nll/relate_tys/placeholder-outlives-existential.rs also loses its leaked binders and now matches the type written in the test's source.

Fixes #134410

This also fixes the leaked outer lifetimes reported in #111365, but I left that issue open since it additionally asks for renaming of shadowed lifetimes (for<'a> fn(for<'a> ...) for two distinct lifetimes both named 'a in the source), which is a separate problem.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 13, 2026
@rustbot

rustbot commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome! The Rust Project is excited to review your changes, and you should hear from @fee1-dead (or someone else) some time within the next two weeks.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue
Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 75 candidates
  • Random selection from 17 candidates

@fee1-dead

Copy link
Copy Markdown
Member

Binders make me a bit dizzy, so...

@rustbot reroll

@rustbot rustbot assigned petrochenkov and unassigned fee1-dead Jul 22, 2026
@petrochenkov

Copy link
Copy Markdown
Contributor

r? types

@rustbot rustbot added the T-types Relevant to the types team, which will review and decide on the PR/issue. label Jul 22, 2026
@rustbot rustbot assigned jackh726 and unassigned petrochenkov Jul 22, 2026
let region = match r.kind() {
ty::ReBound(ty::BoundVarIndexKind::Bound(db), br) if db >= self.current_index => {
*self.region_map.entry(br).or_insert_with(|| name(Some(db), self.current_index, br))
let binder_offset = db.as_u32() - self.current_index.as_u32();

@jackh726 jackh726 Aug 21, 2026

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.

So, this only makes sense until we exit the current binder; if we reenter the binder, then we have an issue.

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.

Agreed. The >= arm was also actively wrong for escaping regions: the lt_idx > binder_level_idx special case returned an INNERMOST region which fold_region then rebound to self.current_index, shifting the escaping region's De Bruijn index down. Dropped the offset keying and moved to ==, which also makes that special case (and the two DebruijnIndex parameters of the naming closure) dead code, so they are removed too. Full tests/ui passes with no other .stderr changes.

let region_map = region_map
.into_items()
.filter_map(|((binder_offset, br), region)| {
(binder_offset == 0).then_some((br, region))

@jackh726 jackh726 Aug 21, 2026

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.

If you always filter by 0, then why are you inserting any others into the map at all?

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.

Good point, there was no reason to. My original thinking was that escaping regions needed a map entry so repeated occurrences get the same name, but by the time the inner binder is folded they already carry a NamedForPrinting kind from the enclosing binder (cmp_fn_sig recurses on the folded value), so the entry never bought anything. With == nothing but the current binder's regions (and placeholders) is inserted, and the offset keying and the filter are gone.

@@ -2732,7 +2738,11 @@ impl<'a, 'tcx> ty::TypeFolder<TyCtxt<'tcx>> for RegionFolder<'a, 'tcx> {
let name = &mut self.name;
let region = match r.kind() {
ty::ReBound(ty::BoundVarIndexKind::Bound(db), br) if db >= self.current_index => {

@jackh726 jackh726 Aug 21, 2026

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's possibly true that the right fix is if db == self.current_index instead of >=.

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.

You are right, that is the simpler and more correct fix: with == the escaping regions are never touched, so they keep their index and the name the enclosing binder already gave them, and they never reach region_map in the first place. Switched to db == self.current_index in the latest push.

@jackh726 jackh726 added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 21, 2026
…08 notes

The `for<...>` prefixes in `cmp_fn_sig` are built from the region map
returned by `name_all_regions`. That map also contained regions that are
bound by an enclosing binder and merely escape through the binder being
named, so nested binders in expected/found notes listed lifetimes they
don't bind, printing invalid types such as
`&mut for<'a> fn(for<'a> fn(&'a ()))` for `&mut for<'a> fn(fn(&'a ()))`.

Key the folder's map by the region's binder offset and only return the
regions actually bound by the binder being named. The offset in the key
also fixes a latent collision between a bound and an escaping region
sharing the same bound variable index. The printed text is unaffected:
the `name` closure already skips escaping regions when writing to the
printer, which is why diagnostic labels were already correct.
@Rani367
Rani367 force-pushed the fix-nested-hrtb-binder-printing branch from a8395d4 to 2686c8c Compare August 25, 2026 15:05
@rustbot

rustbot commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Warning ⚠️

@Rani367

Rani367 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Simplified the fix as suggested: RegionFolder now only folds regions with db == self.current_index, and the offset keying, the map filter, and the now-dead #102392 special case in the naming closure are removed. Updated the PR description to match. Full tests/ui passes locally with no changes to other expected output.

@rustbot review

@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 (such as code changes or more information) from the author. labels Aug 25, 2026
@jackh726

Copy link
Copy Markdown
Member

@bors r+ rollup

@rust-bors

rust-bors Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📌 Commit 2686c8c has been approved by jackh726

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 25, 2026
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Aug 25, 2026
…nting, r=jackh726

Don't list escaping bound regions in nested `for<...>` binders of E0308 notes

The expected/found notes of "one type is more general than the other" errors could print types that are not even valid syntax, repeating lifetimes from an outer binder inside nested `for<...>` lists:

```
= note: expected mutable reference `&mut for<'a> fn(for<'a> fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
```

The expected type here is actually `&mut for<'a> fn(fn(&'a ()))`, the inner fn pointer binds nothing. In the example from rust-lang#111365 the note printed `for<'o> fn(for<'a, 'o> fn(&'a (), &'o ()))` even though the inner binder only binds `'a`.

The cause is in how `cmp_fn_sig` builds its `for<...>` prefixes. It calls `name_all_regions` and joins every region of the returned map into the list. But `RegionFolder` folded every region at or above the depth of the binder being printed (`db >= self.current_index`), so regions that are bound by an enclosing binder and merely escape through the current one also ended up in the map. The pretty printer's own text output already skipped those regions via a special case in the naming closure (which is why the labels of these diagnostics were correct, see rust-lang#102392), but the returned map kept them, and `cmp_fn_sig` recurses through nested fn pointers one binder at a time, so every nested `for<...>` list picked up all the outer lifetimes. That special case also rebound the escaping region to `self.current_index`, i.e. shifted its De Bruijn index down so that it looked like it was bound by the current binder.

This PR makes `RegionFolder` only fold regions bound by the binder being named (`db == self.current_index`). Escaping regions are left untouched: they keep their index, and they already carry a name because the enclosing binder named them when it was folded (`cmp_fn_sig` recurses on the folded value). This makes the rust-lang#102392 special case in the naming closure dead, so it is removed along with the two `DebruijnIndex` parameters it needed.

With this change, the two examples above print:

```
= note: expected mutable reference `&mut for<'a> fn(fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
```

```
= note: expected fn pointer `for<'o> fn(for<'a> fn(&'a (), &'o ()))`
           found fn pointer `fn(for<'a> fn(&'a (), &'a ()))`
```

The `found` line of `tests/ui/nll/relate_tys/placeholder-outlives-existential.rs` also loses its leaked binders and now matches the type written in the test's source.

Fixes rust-lang#134410

This also fixes the leaked outer lifetimes reported in rust-lang#111365, but I left that issue open since it additionally asks for renaming of shadowed lifetimes (`for<'a> fn(for<'a> ...)` for two distinct lifetimes both named `'a` in the source), which is a separate problem.
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Aug 25, 2026
…nting, r=jackh726

Don't list escaping bound regions in nested `for<...>` binders of E0308 notes

The expected/found notes of "one type is more general than the other" errors could print types that are not even valid syntax, repeating lifetimes from an outer binder inside nested `for<...>` lists:

```
= note: expected mutable reference `&mut for<'a> fn(for<'a> fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
```

The expected type here is actually `&mut for<'a> fn(fn(&'a ()))`, the inner fn pointer binds nothing. In the example from rust-lang#111365 the note printed `for<'o> fn(for<'a, 'o> fn(&'a (), &'o ()))` even though the inner binder only binds `'a`.

The cause is in how `cmp_fn_sig` builds its `for<...>` prefixes. It calls `name_all_regions` and joins every region of the returned map into the list. But `RegionFolder` folded every region at or above the depth of the binder being printed (`db >= self.current_index`), so regions that are bound by an enclosing binder and merely escape through the current one also ended up in the map. The pretty printer's own text output already skipped those regions via a special case in the naming closure (which is why the labels of these diagnostics were correct, see rust-lang#102392), but the returned map kept them, and `cmp_fn_sig` recurses through nested fn pointers one binder at a time, so every nested `for<...>` list picked up all the outer lifetimes. That special case also rebound the escaping region to `self.current_index`, i.e. shifted its De Bruijn index down so that it looked like it was bound by the current binder.

This PR makes `RegionFolder` only fold regions bound by the binder being named (`db == self.current_index`). Escaping regions are left untouched: they keep their index, and they already carry a name because the enclosing binder named them when it was folded (`cmp_fn_sig` recurses on the folded value). This makes the rust-lang#102392 special case in the naming closure dead, so it is removed along with the two `DebruijnIndex` parameters it needed.

With this change, the two examples above print:

```
= note: expected mutable reference `&mut for<'a> fn(fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
```

```
= note: expected fn pointer `for<'o> fn(for<'a> fn(&'a (), &'o ()))`
           found fn pointer `fn(for<'a> fn(&'a (), &'a ()))`
```

The `found` line of `tests/ui/nll/relate_tys/placeholder-outlives-existential.rs` also loses its leaked binders and now matches the type written in the test's source.

Fixes rust-lang#134410

This also fixes the leaked outer lifetimes reported in rust-lang#111365, but I left that issue open since it additionally asks for renaming of shadowed lifetimes (`for<'a> fn(for<'a> ...)` for two distinct lifetimes both named `'a` in the source), which is a separate problem.
rust-bors Bot pushed a commit that referenced this pull request Aug 25, 2026
…uwer

Rollup of 5 pull requests

Successful merges:

 - #161328 (Enforce even more library clippy lints in CI)
 - #161684 (Replace `Allocator + Clone` with `AllocatorClone` in btree)
 - #159232 (Don't list escaping bound regions in nested `for<...>` binders of E0308 notes)
 - #161464 (various cleanups of `rustc_builtin_macros`)
 - #161774 (Add codegen test for disjunction fed to unreachable_unchecked)
@rust-bors
rust-bors Bot merged commit 894a7f0 into rust-lang:main Aug 26, 2026
13 checks passed
rust-bors Bot pushed a commit that referenced this pull request Aug 26, 2026
Rollup merge of #159232 - Rani367:fix-nested-hrtb-binder-printing, r=jackh726

Don't list escaping bound regions in nested `for<...>` binders of E0308 notes

The expected/found notes of "one type is more general than the other" errors could print types that are not even valid syntax, repeating lifetimes from an outer binder inside nested `for<...>` lists:

```
= note: expected mutable reference `&mut for<'a> fn(for<'a> fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
```

The expected type here is actually `&mut for<'a> fn(fn(&'a ()))`, the inner fn pointer binds nothing. In the example from #111365 the note printed `for<'o> fn(for<'a, 'o> fn(&'a (), &'o ()))` even though the inner binder only binds `'a`.

The cause is in how `cmp_fn_sig` builds its `for<...>` prefixes. It calls `name_all_regions` and joins every region of the returned map into the list. But `RegionFolder` folded every region at or above the depth of the binder being printed (`db >= self.current_index`), so regions that are bound by an enclosing binder and merely escape through the current one also ended up in the map. The pretty printer's own text output already skipped those regions via a special case in the naming closure (which is why the labels of these diagnostics were correct, see #102392), but the returned map kept them, and `cmp_fn_sig` recurses through nested fn pointers one binder at a time, so every nested `for<...>` list picked up all the outer lifetimes. That special case also rebound the escaping region to `self.current_index`, i.e. shifted its De Bruijn index down so that it looked like it was bound by the current binder.

This PR makes `RegionFolder` only fold regions bound by the binder being named (`db == self.current_index`). Escaping regions are left untouched: they keep their index, and they already carry a name because the enclosing binder named them when it was folded (`cmp_fn_sig` recurses on the folded value). This makes the #102392 special case in the naming closure dead, so it is removed along with the two `DebruijnIndex` parameters it needed.

With this change, the two examples above print:

```
= note: expected mutable reference `&mut for<'a> fn(fn(&'a ()))`
           found mutable reference `&mut fn(fn(&()))`
```

```
= note: expected fn pointer `for<'o> fn(for<'a> fn(&'a (), &'o ()))`
           found fn pointer `fn(for<'a> fn(&'a (), &'a ()))`
```

The `found` line of `tests/ui/nll/relate_tys/placeholder-outlives-existential.rs` also loses its leaked binders and now matches the type written in the test's source.

Fixes #134410

This also fixes the leaked outer lifetimes reported in #111365, but I left that issue open since it additionally asks for renaming of shadowed lifetimes (`for<'a> fn(for<'a> ...)` for two distinct lifetimes both named `'a` in the source), which is a separate problem.
@rustbot rustbot added this to the 1.100.0 milestone Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error mentions invalid nested HRTB: &mut for<'a> fn(for<'a> fn(&'a ()))

5 participants