`-Znext-solver`: TAIT hidden closure type recorded twice with different parent regions ("concrete type differs from previous defining opaque type use")
Nobody has claimed this yet.
- 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
- In case 1,
expectedandgotprint the identical string, so the message alone gives the reader nothing to act on.-Zverbose-internalsis required to see that the difference is'{erased}vs'ain the closure's parent args. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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