Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type inference of lambda arguments interferes with borrow checking #62640

Open
sigfriedmcwild opened this issue Jul 12, 2019 · 9 comments
Open
Labels
A-borrow-checker Area: The borrow checker A-closures Area: Closures (`|…| { … }`) A-inference Area: Type inference C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@sigfriedmcwild
Copy link

It looks like type inference of arguments to a lambda function somehow interferes with borrow checking.

I tried this code: (playground link)

struct Bar {}

struct Foo {
    a: Bar,
}

impl Foo {
    fn one(&self) -> Option<&Bar> {
        return Some(&self.a);
    }

    fn two(&mut self) -> Option<Bar> {
        return Some(Bar {});
    }
}

fn main() {
    let mut f = Foo { a: Bar {} };

    let test = |_t| {};

    match f.one() {
        Some(a) => test(a),
        None => {}
    }

    match f.two() {
        Some(ref b) => test(b),
        None => {}
    }
}

I expected to see this happen:

A successful build

Instead, this happened:

Compilation failed with this error:

error[E0502]: cannot borrow `f` as mutable because it is also borrowed as immutable
  --> src/lib.rs:27:11
   |
22 |     match f.one() {
   |           - immutable borrow occurs here
...
27 |     match f.two() {
   |           ^^^^^^^ mutable borrow occurs here
28 |         Some(ref b) => test(b),
   |                        ---- immutable borrow later used here

Note that changing line 20 to

let test = |_t: &Bar| {};

results in a successful compilation (playground link)

Meta

rustc --version --verbose:

rustc 1.36.0 (a53f9df32 2019-07-03)
binary: rustc
commit-hash: a53f9df32fbb0b5f4382caaad8f1a46f36ea887c
commit-date: 2019-07-03
host: x86_64-pc-windows-msvc
release: 1.36.0
LLVM version: 8.0
@jonas-schievink jonas-schievink added A-borrow-checker Area: The borrow checker A-closures Area: Closures (`|…| { … }`) A-inference Area: Type inference C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 14, 2019
@xjkdev
Copy link

xjkdev commented Aug 6, 2020

I had a similar issue too.
Without type inference in foo, when calling foo twice, this code compiles.

fn main() {
    let mut a = 1;
    let b = &mut a;
    let foo = |state: &mut i32| {
        Box::new(state);
    };

    foo(b);
    foo(b);
}

However, if I leave state in foo to be type inferred, compiler complains a borrow-after-move error.

fn main() {
    let mut a = 1;
    let b = &mut a;
    let foo = |state| {
        Box::new(state);
    };

    foo(b);
    foo(b);
}

I dug into Mir of both code snippet, and found out that the type of state in the second code had expectedly inferred as &mut i32. In my opinion, there may be a bug in borrow checking.

rustc --version --verbose:

rustc 1.45.2 (d3fb005a3 2020-07-31)
binary: rustc
commit-hash: d3fb005a39e62501b8b0b356166e515ae24e2e54
commit-date: 2020-07-31
host: x86_64-apple-darwin
release: 1.45.2
LLVM version: 10.0

@hyuuko1
Copy link

hyuuko1 commented Aug 7, 2020

I think that is because when you specify the type, reborrow occurs. When you don't specify the type, the mutable reference will be moved into the function.

@xjkdev
Copy link

xjkdev commented Aug 7, 2020

@Hyuuko As far as I know, when calling functions or closures, the default behavior is to reborrow references into parameters. If the reference had been moved, it seems like a bug or a rare behavior to me. Is there any document addressing this move behavior?

@carols10cents
Copy link
Member

carols10cents commented Aug 23, 2021

Yeah, I just got bit by this in a very similar example, and I'm a bit disturbed because if this behavior is expected and type annotation is required in some closures, then I need to change some language in TRPL, like:

Closures don’t require you to annotate the types of the parameters or the return value like fn functions do.

Within these limited contexts, the compiler is reliably able to infer the types of the parameters and the return type

As with variables, we can add type annotations if we want to increase explicitness and clarity at the cost of being more verbose than is strictly necessary.

@matthew-mcallister
Copy link
Contributor

Even stranger, type inference affects how borrows are treated inside a closure.

This code fails to compile:

fn main() {
    fn consumer(var: &mut i32) {
        *var += 1;
    }
    let closure = |var| {
        consumer(var);
        consumer(var);
    };
    
    let mut x: i32 = 0;
    closure(&mut x);
}

Output:

error[E0382]: borrow of moved value: `var`
 --> src/main.rs:7:18
  |
5 |     let closure = |var| {
  |                    --- move occurs because `var` has type `&mut i32`, which does not implement the `Copy` trait
6 |         consumer(var);
  |                  --- value moved here
7 |         consumer(var);
  |                  ^^^ value borrowed here after move

Annotating var as &mut i32 fixes things.

This kind of cripples type inference for mutable references.

@TinusgragLin
Copy link

TinusgragLin commented Jan 26, 2023

Even stranger, type inference affects how borrows are treated inside a closure.

This code fails to compile:

fn main() {
    fn consumer(var: &mut i32) {
        *var += 1;
    }
    let closure = |var| {
        consumer(var);
        consumer(var);
    };
    
    let mut x: i32 = 0;
    closure(&mut x);
}

Output:

error[E0382]: borrow of moved value: `var`
 --> src/main.rs:7:18
  |
5 |     let closure = |var| {
  |                    --- move occurs because `var` has type `&mut i32`, which does not implement the `Copy` trait
6 |         consumer(var);
  |                  --- value moved here
7 |         consumer(var);
  |                  ^^^ value borrowed here after move

Annotating var as &mut i32 fixes things.

This kind of cripples type inference for mutable references.

This happens even when the closure is never used, the problem seems to be that when the type of var is not given, the compiler moves var into the first consumer function, instead of using reborrow, my guess would be that if reborrow is used, then var could be a mutable reference to any type that implements DerefMut<Target=i32> according to the reference, so the compiler won't be able to determine the type of var. But again, I don't see why the compiler can't first determine the type of var through the use of closure, then use reborrow for both calls to consumer inside the closure.

@sigfriedmcwild
Copy link
Author

sigfriedmcwild commented Apr 15, 2024

I was playing around to see if this issue still reproed and crafted a more minimal example (playground link):

struct Bar {}

fn foo() -> Bar { Bar {} }

fn main() {
    let test = |_t| {};
    
    test(&foo());
    test(&foo());
}

The compiler error is slightly different, but I'm pretty sure the underlying issue is the same

error[[E0716]](https://doc.rust-lang.org/nightly/error_codes/E0716.html): temporary value dropped while borrowed
 --> src/main.rs:8:11
  |
8 |     test(&foo());
  |           ^^^^^ - temporary value is freed at the end of this statement
  |           |
  |           creates a temporary value which is freed while still in use
9 |     test(&foo());
  |     ---- borrow later used here
  |
help: consider using a `let` binding to create a longer lived value
  |
8 ~     let binding = foo();
9 ~     test(&binding);
  |

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

As before annotating the lambda type to be &Bar fixes the issue (and so does using 2 separate variables to store results of the 2 calls to f.one())

@TinusgragLin
Copy link

For anyone trying to understand why this happens, I found #100002 a year ago when I encountered this strange behavior myself.

The "But Why?" section explain the underlying compiler decision that causes this problem. If I understand it correctly, it says that when the argument of a closure contains reference but is not type-annotated, the lifetime of this reference would be a lifetime parameter of the closure and thus must be as long as the lifetime of the closure, when it is type-annotated, the lifetime is higher-ranked, i.e. it can be any lifetime and is determined every time the closure is called.

let mut x = 42;

let f = |x| {}; // ┬
f(&x);          // │ the lifetime of f
&mut x; // ┐       │
f;      // │       ┴
        // Err: can not borrow as mutable as it is also borrowed immutably

Stabilization of #97362 is probably going to mitigate this rather "dumb" rule.

@jdahlstrom
Copy link

jdahlstrom commented Dec 13, 2024

Another slightly different repro:

fn g<T, F: Fn(T)>(_: T, _: F) {}

fn main() {
    let f = |_| ();
    // let f = |_: &_| (); // <-- this works

    for i in 1..1 {
        g(&vec![i], f);
    }
}

It certainly took me a while to find the culprit in my significantly more complex code…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-borrow-checker Area: The borrow checker A-closures Area: Closures (`|…| { … }`) A-inference Area: Type inference C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

8 participants