rust-lang / rust-lang/rust

fn-item to fn-pointer coercion fails with E0271 when the fn item is wrapped with array + closure

Open
#158,653 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/functions-closures/fn-bare-spawn.rs:

fn spawn<T: Send>(val: T, f: fn(T)) { f(val); }

fn f(i: isize) { assert_eq!(i, 100); }

pub fn main() {
    spawn(100, f); // fn item `f` is coerced to the fn pointer `fn(isize)` at the argument site
}

The argument f is an fn item of fn(isize) {f}; it is coerced to the fn pointer fn(isize) because the parameter's expected type is fn(T).
This coercion is driven by the expected type.

Now route the same fn item through a closure F: FnOnce() -> fn(T) and wrap that closure in a one-element array [F; 1] (destructured in the body with let [f0] = _f0;, call-site closure wrapped in [ … ]).
The expected type fn(T) is still there but inference no longer performs the coercion:

fn spawn<T: Send, F: FnOnce() -> fn(T)>(val: T, _f0: [F; 1]) {
    let [f0] = _f0;
    let f = f0();
    f(val);
}

fn f(i: isize) { assert_eq!(i, 100); }

pub fn main() {
    spawn(100, [move || f]); // <-- error[E0271]
}
Expected

Both versions compile.

The compiler has exactly the same information, as the expected type still reaches f:

  1. the array argument has expected type [F; 1], so its sole element has expected type F.
  2. F still carries the bound F: FnOnce() -> fn(T), which fixes the closure's return type to fn(T).
  3. so the closure body f has expected type fn(T).
Actual

The array-wrapped version is rejected with a type mismatch between the fn item and the fn pointer it should have been coerced to:

error[E0271]: expected `{closure@src/main.rs:10:17}` to return `fn({integer})`, but it returns `fn(isize) {f}`
  --> src/main.rs:10:25
   |
10 |     spawn(100, [move || f]);
   |     -----       ------- ^ expected fn pointer, found fn item
   |     |           |
   |     |           this closure
   |     required by a bound introduced by this call
   |
   = note: expected fn pointer `fn({integer})`
                 found fn item `fn(isize) {f}`
note: required by a bound in `spawn`
  --> src/main.rs:1:34
   |
 1 | fn spawn<T: Send, F: FnOnce() -> fn(T)>(val: T, _f0: [F; 1]) {
   |                                  ^^^^^ required by this bound in `spawn`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0271`.

The closure is inferred to return the fn item's own type fn(isize) {f} instead of being coerced to the expected fn(isize) pointer - i.e. the expected return type fn(T) did not reach the closure body through the array element.

Testing with different wrapper types
// direct F - compiles
fn spawn<T: Send, F: FnOnce() -> fn(T)>(val: T, _f0: F)    { let f = _f0();     f(val); }
// (F,) - compiles
fn spawn<T: Send, F: FnOnce() -> fn(T)>(val: T, _f0: (F,)) { let f = (_f0.0)(); f(val); }
// [F; 1] - fails E0271
fn spawn<T: Send, F: FnOnce() -> fn(T)>(val: T, _f0: [F; 1]) { let [f0] = _f0; let f = f0(); f(val); }

fn f(i: isize) { assert_eq!(i, 100); }
// main, respectively:
//   spawn(100, move || f);
//   spawn(100, (move || f,));
//   spawn(100, [move || f]);

A reference &F also compiles (with the bound relaxed to F: Fn() -> fn(T), since a FnOnce cannot be called through &F).

With explicit type annotations, the wrapped form compiles

The same [F; 1]-wrapped form compiles once the closure's return type is supplied explicitly, by either of two routes:

// concrete array element type --> the coercion fires through the array literal + closure body
fn f(i: isize) { assert_eq!(i, 100); }
fn main() {
    let _x: [fn() -> fn(isize); 1] = [move || f]; // compiles (the coercion fires through the annotation)
    let _ = _x;
}
// explicit closure return annotation supplies the expected type by hand
fn spawn<T: Send, F: FnOnce() -> fn(T)>(val: T, _f0: [F; 1]) {
    let [f0] = _f0; let f = f0(); f(val);
}
fn f(i: isize) { assert_eq!(i, 100); }
fn main() {
    spawn(100isize, [move || -> fn(isize) { f }]); // compiles
}

Both compile. The failure appears only when the closure's return type has no source other than being inferred through the [F; 1] element from the callee bound.

Notes

Possibly related to #157468, another [F; 1]-wrapping case involving a callee Fn/FnOnce bound, which surfaces as E0282. I am filing this as a separate issue because the symptom differs - the error code here is E0271 rather than E0282.

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 minimal direct, tuple, and [F; 1] examples, comparing them with tests/ui/functions-closures/fn-bare-spawn.rs. Use the explicit annotation cases to isolate the missing coercion behavior. Done means the array-wrapped closure compiles like the direct and tuple forms, with regression coverage for the E0271 case.

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
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.