Closure parameter type not inferred from `Fn` bound through a `FnOnce() -> T` wrapper
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Code
This program compiles and runs — it is rustc's own UI test tests/ui/unboxed-closures/unboxed-closures-infer-arg-types-from-expected-object-type.rs, which exists specifically to check that a closure's argument type is inferred from an expected Fn bound:
pub trait ToPrimitive {
fn to_int(&self) {}
}
impl ToPrimitive for isize {}
impl ToPrimitive for i32 {}
impl ToPrimitive for usize {}
fn doit<T>(val: T, f: &dyn Fn(T)) {
f(val)
}
pub fn main() {
doit(0, &|x| { x.to_int(); }); // x inferred as isize via Fn(T), T = {integer}
}
If we wrapped the eager val: T argument into a zero-argument closure f0: F where F: FnOnce() -> T, and rewrote the call site from 0 to || 0, the inner closure parameter x becomes un-inferable:
pub trait ToPrimitive {
fn to_int(&self) {}
}
impl ToPrimitive for isize {}
impl ToPrimitive for i32 {}
impl ToPrimitive for usize {}
fn doit<T, F: FnOnce() -> T>(f0: F, f: &dyn Fn(T)) {
let val = f0();
f(val)
}
pub fn main() {
doit(|| 0, &|x| { x.to_int(); }); // <-- error[E0282] on `x`
}
Expected
Both versions compile, because the rewritten program gives the compiler the same information, just through one extra layer of indirection. In the original, T is the type of the value 0; in the rewrite, T is the return type of the wrapping closure || 0 (via F: FnOnce() -> T) — equally determined. In both programs the inner closure's parameter is T through the Fn(T) bound, so x is just as inferable: T is fixed by the first argument, and Fn(T) ties the closure parameter to T.
Actual
error[E0282]: type annotations needed
--> src/main.rs:15:18
|
15 | doit(|| 0, &|x| { x.to_int(); });
| ^ - type must be known at this point
|
help: consider giving this closure parameter an explicit type
|
15 | doit(|| 0, &|x: /* Type */| { x.to_int(); });
| ++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0282`.
Notes
The dyn trait object is not the cause. Changing f: &dyn Fn(T) to a generic trait bound f: &G with G: Fn(T) reproduces the identical E0282:
pub trait ToPrimitive {
fn to_int(&self) {}
}
impl ToPrimitive for isize {}
impl ToPrimitive for i32 {}
impl ToPrimitive for usize {}
fn doit<T, F: FnOnce() -> T, G: Fn(T)>(f0: F, f: &G) {
let val = f0();
f(val)
}
pub fn main() {
doit(|| 0, &|x| { x.to_int(); }); // <-- error[E0282] on `x`, identical to above
}
error[E0282]: type annotations needed
--> src/main.rs:15:18
|
15 | doit(|| 0, &|x| { x.to_int(); });
| ^ - type must be known at this point
What does matter is the reference. Passing the consuming closure by value (f: G with G: Fn(T), called with a bare |x| …) routes the expected type through, and the program compiles:
fn doit<T, F: FnOnce() -> T, G: Fn(T)>(f0: F, f: G) {
let val = f0();
f(val)
}
pub fn main() {
doit(|| 0, |x| { x.to_int(); }); // compiles: x inferred as the integer default
}
The closest existing issue is #112225, which similarly wraps the first argument — there, in an async block — and fails with E0282. That issue was fixed in 1.71.1, but the case here still reproduces on current stable (1.96.0).
Meta
rustc 1.96.0 (ac68faa20 2026-05-25)
binary: rustc
commit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96
commit-date: 2026-05-25
host: aarch64-apple-darwin
release: 1.96.0
LLVM version: 22.1.2
Tested on stable 1.96.0 only.
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 the reduced examples and running tests/ui/unboxed-closures/unboxed-closures-infer-arg-types-from-expected-object-type.rs. Compare inference for the by-reference and by-value closure cases, then inspect the compiler path handling expected types for Fn and FnOnce bounds. Done means the wrapped closure case compiles and has a regression test without breaking the existing UI test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100