rust-lang / rust-lang/rust

How can `let y;` and `let y: _;` be different to the borrow checker?

Open
#138,194 4 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-borrow-checker C-discussion T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

@rustbot label A-borrow-checker, C-discussion

I’ve tested across this code example when trying to understand corner-cases of borrow checker behavior:

fn test() {
    let x = Box::new(1);
    let y = &(&*x, 123);
    let r = &y.1;
    drop(x);
    println!("{r}");
}

The fact that this does work in the first place is slightly cool; given that the type of y, &'a (&'b i32, i32) would usually come with an 'b: 'a bound, but the rest of the code make it so that the re-borrow in r (which could have lifetime &'a i32) lives longer than the Box that the inner &'b i32 comes from.

But then I noticed – slightly disappointed – that

fn test() {
    let x = Box::new(1);
    let y: _ = &(&*x, 123);
    let r = &y.1;
    drop(x);
    println!("{r}");
}

no longer works! Now, I first thought… such type annotations can sometimes reasonably have significant effects on the meaning of the code, in particular some let y: &_ = …; usually starts allowing y to be created through a reborrow … at least for &mut T that’s sometimes very relevant.

But here, I’m not actually providing any concrete type in the first place. So why would the added : _ still make any difference?

For a slight variation with the same effect, these two have the same distinction:

fn test() {
    let x = Box::new(1);
    let y_target = (&*x, 123);
    let y;
    y = &y_target;
    let r = &y.1;
    drop(x);
    println!("{r}");
}

behavior:
compiles successfully


vs

fn test() {
    let x = Box::new(1);
    let y_target = (&*x, 123);
    let y: _;
    y = &y_target;
    let r = &y.1;
    drop(x);
    println!("{r}");
}

behavior:

error[E0505]: cannot move out of `x` because it is borrowed
 --> src/lib.rs:7:10
  |
2 |     let x = Box::new(1);
  |         - binding `x` declared here
3 |     let y_target = (&*x, 123);
  |                     --- borrow of `*x` occurs here
...
7 |     drop(x);
  |          ^ move out of `x` occurs here
8 |     println!("{r}");
  |               --- borrow later used here
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 -     let y_target = (&*x, 123);
3 +     let y_target = (&x.clone(), 123);
  |

(playground)

It seems surprising that let y; and let y: _; aren’t fully equivalent; in my mind, they should simply both leave the type of y to-be-inferred for later. [Still, I’d also suspect that “implementation details of type inference” + “interaction of type information with the borrow checker” somehow forms the answer to this.]

I’m leaving this with just C-discussion for now; I haven’t found this reported elsewhere here on the rust-lang/rust issue tracker (as far as the search bar & labels could tell me). If someone else deems this to be a bug and/or well-fixable, we can turn it into C-bug as well 😇

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

The issue names no repository files or tests; start by reproducing the paired examples in the linked Rust Playground and compare the compiler's inferred types and borrow-checking behavior. Done should be a clear explanation of the distinction, or a narrowed compiler bug report if the behavior is unintended.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.