Skip to content

Confusing diagnostics resulting from a function with the bound T: FnOnce() -> T that takes T and returns T #80638

Description

@PatchMixolydic

I tried this code (playground):

fn closure_ret_closure<T: FnOnce() -> T>(f: T) -> T {
    f()
}

fn main() {
    closure_ret_closure(|| 4);
}

I expected to see this happen:

Either a diagnostic proclaiming that the type parameter for closure_ret_closure is recursive or a diagnostic that clearly states that || 4 does not return FnOnce() -> FnOnce() -> FnOnce() -> ....

Instead, this happened:

Two seemingly conflicting diagnostics are emitted:

error[E0277]: expected a `FnOnce<()>` closure, found `{integer}`
 --> src/main.rs:6:5
  |
1 | fn closure_ret_closure<T: FnOnce() -> T>(f: T) -> T {
  |                           ------------- required by this bound in `closure_ret_closure`
...
6 |     closure_ret_closure(|| 4);
  |     ^^^^^^^^^^^^^^^^^^^ expected an `FnOnce<()>` closure, found `{integer}`
  |
  = help: the trait `FnOnce<()>` is not implemented for `{integer}`
  = note: wrap the `{integer}` in a closure with no arguments: `|| { /* code */ }`

error[E0308]: mismatched types
 --> src/main.rs:6:25
  |
6 |     closure_ret_closure(|| 4);
  |                         ^^^^ expected integer, found closure
  |
  = note: expected type `{integer}`
          found closure `[closure@src/main.rs:6:25: 6:29]`

The first diagnostic claims that {integer}, the type of the value returned by the closure, is not a FnOnce() -> .... It is not immediately clear that this is in reference to the return type, so the user may believe the value in question is the parameter.

The second diagnostic claims that it expects the parameter to be of type {integer}. This is quite strange, because the previous diagnostic just told us that T: FnOnce() -> FnOnce() -> ..., and {integer} does not fit that requirement.

What may be happening is that since the result of calling || 4 is of type {integer}, the compiler believes T must be {integer}. Since {integer} does not implement FnOnce() -> {integer}, it doesn't fit the bounds on T, generating the first diagnostic. Then, the compiler sees that || 4 is not a valid argument to closure_ret_closure, since f: T and for this monomorphization, T = {integer}.

Even writing out the logic behind this error is quite confusing, so it would be helpful to improve these diagnostics in case a user accidentally stumbles upon them. I'm not entirely sure how to accomplish this, but making it clearer that the first diagnostic (E0277) is referring to the return type might help.

Unfortunately, I'm not sure if anything can be done regarding the rather strange T: FnOnce() -> T bound, since it's quite possible to implement a type that can satisfy T: FnOnce() -> T on nightly. This compiles and runs successfully (playground):

#![feature(fn_traits)]
#![feature(unboxed_closures)]

struct Foo;

impl FnOnce<()> for Foo {
    type Output = Self;

    extern "rust-call" fn call_once(self, _: ()) -> Self::Output {
        self
    }
}

fn closure_ret_closure<T: FnOnce() -> T>(f: T) -> T {
    f()
}

fn main() {
    closure_ret_closure(Foo);
}

Since (as far as I know) FnOnce can only be implemented on nightly, we may be able to get away with making trait bounds like T: FnOnce() -> T a deny-by-default lint, or at least warn-by-default. I'm not certain if this would be entirely feasible/possible with the current trait system, however, since that would be equivalent to linting against this:

trait Foo {
    type Bar;
}

fn baz<T: Foo<Bar = T>>(_: T) { unimplemented!() }

Meta

rustc --version --verbose:

rustc 1.48.0 (7eac88abb 2020-11-16)
binary: rustc
commit-hash: 7eac88abb2e57e752f3302f02be5f3ce3d7adfb4
commit-date: 2020-11-16
host: x86_64-unknown-linux-gnu
release: 1.48.0
LLVM version: 11.0

rustc +nightly --version --verbose:

rustc 1.51.0-nightly (257becbfe 2020-12-27)
binary: rustc
commit-hash: 257becbfe4987d1f7b12af5a8dd5ed96697cd2e8
commit-date: 2020-12-27
host: x86_64-unknown-linux-gnu
release: 1.51.0-nightly

@rustbot modify labels: +A-diagnostics +A-traits +C-enhancement +D-confusing +T-compiler

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-closuresArea: Closures (`|…| { … }`)A-diagnosticsArea: Messages for errors, warnings, and lintsA-trait-systemArea: Trait systemC-enhancementCategory: An issue proposing an enhancement or a PR with one.D-confusingDiagnostics: Confusing error or lint that should be reworked.D-verboseDiagnostics: Too much output caused by a single piece of incorrect code.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions