rust-lang / rust-lang/rust

Closure parameter type not inferred from `Fn` bound through a `FnOnce() -> T` wrapper

Open
#157,469 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug needs-triage
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

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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.