rust-lang / rust-lang/rust

`-Znext-solver`: TAIT hidden closure type recorded twice with different parent regions ("concrete type differs from previous defining opaque type use")

Open
#161,816 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-impl-trait A-trait-system C-bug D-confusing D-imprecise-spans F-type_alias_impl_trait needs-triage requires-nightly T-types WG-trait-system-refactor
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Summary

With the new trait solver, a TAIT whose hidden type is a closure defined inside a function that is itself generic over a lifetime can be rejected with:

error: concrete type differs from previous defining opaque type use

where both printed types are identical. Under -Zverbose-internals the two recorded hidden types differ only in the parent generic args of the closure: one has the lifetime erased, the other has the real region:

expected `{Task<'{erased}>::new::{closure#0} closure_kind_ty=i8 closure_sig_as_fn_ptr_ty=extern "rust-call" fn(()) upvar_tys=()}`
got      `{Task<'a/#1>::new::{closure#0}     closure_kind_ty=i8 closure_sig_as_fn_ptr_ty=extern "rust-call" fn(()) upvar_tys=()}`

The old solver accepts the same code.

Case 1

#![feature(type_alias_impl_trait)]

use core::marker::PhantomData;

pub struct Handle<'a, F>(PhantomData<&'a mut F>);

pub fn spawn<'a, F>(_f: F) -> Result<Handle<'a, F>, ()> {
    Err(())
}

type Lam<'a> = impl FnOnce() + 'a;

pub struct Task<'a> {
    _handle: Handle<'a, Lam<'a>>,
}

impl<'a> Task<'a> {
    #[define_opaque(Lam)]
    pub fn new() -> Result<Self, ()> {
        Ok(Self {
            _handle: spawn(|| {})?,
        })
    }
}

Current output:

error: concrete type differs from previous defining opaque type use
  --> src/lib.rs:20:12
   |
20 |           Ok(Self {
   |  ____________^
21 | |             _handle: spawn(|| {})?,
22 | |         })
   | |         ^
   | |         |
   | |_________expected `{closure@src/lib.rs:21:28: 21:30}`, got `{closure@src/lib.rs:21:28: 21:30}`
   |           this expression supplies two conflicting concrete types for the same opaque type

With -Zverbose-internals, the same error prints:

   | |_________expected `{Task<'{erased}>::new::{closure#0} closure_kind_ty=i8 closure_sig_as_fn_ptr_ty=extern "rust-call" fn(()) upvar_tys=()}`, got `{Task<'a/#1>::new::{closure#0} closure_kind_ty=i8 closure_sig_as_fn_ptr_ty=extern "rust-call" fn(()) upvar_tys=()}`

Case 2

A smaller form of the same problem. Neither the impl block, nor Result/?, nor invariance of Handle is needed:

#![feature(type_alias_impl_trait)]

use core::marker::PhantomData;

pub struct Handle<'a, F>(PhantomData<(&'a (), F)>);

pub fn spawn<'a, F>(_f: F) -> Handle<'a, F> {
    loop {}
}

type Lam<'a> = impl FnOnce() + 'a;

pub struct Task<'a> {
    _handle: Handle<'a, Lam<'a>>,
}

#[define_opaque(Lam)]
pub fn new<'a>() -> Task<'a> {
    Task {
        _handle: spawn(|| {}),
    }
}

Current output, unchanged by -Zverbose-internals:

error: concrete type differs from previous defining opaque type use
   |
note: previous use here
  --> src/lib.rs:19:5
   |
19 | /     Task {
20 | |         _handle: spawn(|| {}),
21 | |     }
   | |_____^

Expected output

Both compile, as they do with the old solver.

What the two cases need

The hidden type has to be a closure (substituting a non-closure such as () for it compiles), and the defining function has to construct the wrapper struct Task<'a> { _handle: Handle<'a, Lam<'a>> }. Returning the Handle<'a, Lam<'a>> directly compiles in both cases. The error is also reported on the struct-construction expression, which does not itself mention the closure, so that extra layer seems to be involved in the second registration.

Diagnostic quality

  1. In case 1, expected and got print the identical string, so the message alone gives the reader nothing to act on. -Zverbose-internals is required to see that the difference is '{erased} vs 'a in the closure's parent args.
  2. In case 2, the error has no primary span at all -- there is no --> line for the error itself, only for the note -- and no expected/got types are printed, even under -Zverbose-internals.

Versions tested

rustc 1.100.0-nightly (787af2b8c 2026-08-25)

Found on 1.100 nightly, where the new solver is on by default. Additionally, 1.96.0-nightly (362211dc2 2026-03-24) and 1.93.0-nightly (25d319a0f 2025-11-11) compile both cases by default, and reject them the same way under -Znext-solver=globally.

AI usage disclosure

The reduction from the original crate to the two cases here, and the first draft of this issue text, were produced by Claude (Claude Code) at my direction. All compiler output quoted above was produced by actually running the listed commands, and I have reviewed the report before filing.

@rustbot label +C-bug +T-types +A-trait-system +A-impl-trait +F-type_alias_impl_trait +WG-trait-system-refactor +D-confusing +D-imprecise-spans

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing both cases from the issue in src/lib.rs with -Znext-solver, comparing them with the old solver and using -Zverbose-internals. Investigate the two registrations of the TAIT closure during the Task struct construction; done means both cases compile under the new solver and the conflicting-type diagnostics have useful spans and details.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.