How can `let y;` and `let y: _;` be different to the borrow checker?
Nobody has claimed this yet.
- 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);
|
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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