Hello all,
I try to understand how the compiler deals with lifetimes annotations. For that I try to understand 2 problems.
Problem 1: Let's consider following functions
fn foo() {
let _x: i32 = 123;
let _y: &i32 = &_x;
}
fn bar<'a>() {
let _x: i32 = 123;
let _y: &'a i32 = &_x;
}
the reference _y
has implicite lifetime in foo
, so I tried to express it explicite in bar
, however it doesn't compile
because "_x
not live long enough". Trying to understand why borrow checker complies I ended up with following MIR outputs:
// MIR for `foo` 0 renumber
| User Type Annotations
| 0: user_ty: Canonical { value: Ty(i32), max_universe: U0, variables: [], defining_opaque_types: [] }, span: src/main.rs:41:13: 41:16, inferred_ty: i32
| 1: user_ty: Canonical { value: Ty(i32), max_universe: U0, variables: [], defining_opaque_types: [] }, span: src/main.rs:41:13: 41:16, inferred_ty: i32
| 2: user_ty: Canonical { value: Ty(&i32), max_universe: U0, variables: [CanonicalVarInfo { kind: Region(U0) }], defining_opaque_types: [] }, span: src/main.rs:42:13: 42:17, inferred_ty: &i32
| 3: user_ty: Canonical { value: Ty(&i32), max_universe: U0, variables: [CanonicalVarInfo { kind: Region(U0) }], defining_opaque_types: [] }, span: src/main.rs:42:13: 42:17, inferred_ty: &i32
|
fn foo() -> () {
let mut _0: ();
let _1: i32 as UserTypeProjection { base: UserType(0), projs: [] };
let _3: &i32;
scope 1 {
debug _x => _1;
let _2: &i32 as UserTypeProjection { base: UserType(2), projs: [] };
scope 2 {
debug _y => _2;
}
}
bb0: {
StorageLive(_1);
_1 = const 123_i32;
FakeRead(ForLet(None), _1);
AscribeUserType(_1, o, UserTypeProjection { base: UserType(1), projs: [] });
StorageLive(_2);
StorageLive(_3);
_3 = &_1;
_2 = &(*_3);
FakeRead(ForLet(None), _2);
AscribeUserType(_2, o, UserTypeProjection { base: UserType(3), projs: [] });
StorageDead(_3);
_0 = const ();
StorageDead(_2);
StorageDead(_1);
return;
}
}
// MIR for `bar` 0 renumber
| User Type Annotations
| 0: user_ty: Canonical { value: Ty(i32), max_universe: U0, variables: [], defining_opaque_types: [] }, span: src/main.rs:46:13: 46:16, inferred_ty: i32
| 1: user_ty: Canonical { value: Ty(i32), max_universe: U0, variables: [], defining_opaque_types: [] }, span: src/main.rs:46:13: 46:16, inferred_ty: i32
| 2: user_ty: Canonical { value: Ty(&'a i32), max_universe: U0, variables: [], defining_opaque_types: [] }, span: src/main.rs:47:13: 47:20, inferred_ty: &i32
| 3: user_ty: Canonical { value: Ty(&'a i32), max_universe: U0, variables: [], defining_opaque_types: [] }, span: src/main.rs:47:13: 47:20, inferred_ty: &i32
|
fn bar() -> () {
let mut _0: ();
let _1: i32 as UserTypeProjection { base: UserType(0), projs: [] };
let _3: &i32;
scope 1 {
debug _x => _1;
let _2: &i32 as UserTypeProjection { base: UserType(2), projs: [] };
scope 2 {
debug _y => _2;
}
}
bb0: {
StorageLive(_1);
_1 = const 123_i32;
FakeRead(ForLet(None), _1);
AscribeUserType(_1, o, UserTypeProjection { base: UserType(1), projs: [] });
StorageLive(_2);
StorageLive(_3);
_3 = &_1;
_2 = &(*_3);
FakeRead(ForLet(None), _2);
AscribeUserType(_2, o, UserTypeProjection { base: UserType(3), projs: [] });
StorageDead(_3);
_0 = const ();
StorageDead(_2);
StorageDead(_1);
return;
}
}
The bb0
blocks are the same for foo
and bar
the only difference is in types 3
and 4
, where Ty(&'a i32)
causes compilation error. Can someone explain or point to explanation for this behaviour?
Problem 2. Let's take the rust book lifetime example:
a)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
on the other hand we can write this in following way
b)
fn longest<'a, 'b, 'c>(x: &'a str, y: &'b str) -> &'c str
where: 'a: 'c, 'b: 'c
{
if x.len() > y.len() {
x
} else {
y
}
}
which means that 'a
-lifetime and 'b
-lifetime must outlive the 'c
-lifetime. Here we have freedom of choosing a and b lifetimes, they are independent. When we assumed that 'a = 'b
that implies 'c = 'a
and we end up in 2a example. Why compiler narrow down the lifetimes in such way? In MIR
// MIR for `longest` 0 renumber
fn longest(_1: &str, _2: &str) -> &str {
debug x => _1;
debug y => _2;
let mut _0: &str;
let mut _3: bool;
let mut _4: usize;
let mut _5: &str;
let mut _6: usize;
let mut _7: &str;
bb0: {
StorageLive(_3);
StorageLive(_4);
StorageLive(_5);
_5 = &(*_1);
_4 = core::str::<impl str>::len(move _5) -> [return: bb1, unwind: bb6];
}
bb1: {
StorageDead(_5);
StorageLive(_6);
StorageLive(_7);
_7 = &(*_2);
_6 = core::str::<impl str>::len(move _7) -> [return: bb2, unwind: bb6];
}
bb2: {
StorageDead(_7);
_3 = Gt(move _4, move _6);
switchInt(move _3) -> [0: bb4, otherwise: bb3];
}
bb3: {
StorageDead(_6);
StorageDead(_4);
_0 = &(*_1);
goto -> bb5;
}
bb4: {
StorageDead(_6);
StorageDead(_4);
_0 = &(*_2);
goto -> bb5;
}
bb5: {
StorageDead(_3);
return;
}
bb6 (cleanup): {
resume;
}
}
I would expect sth like Ty(&'a str)
, similar to problem 1, however there is nothing like that. Still no clue why.
Besides those problems, is there any good tool or way to intercept the borrow checker logic during compilation? The MIR analysis doesn't give always the straight answear