The behavior described in StatementKind suggest that the pair (StorageLive / StorageDead) determines the lifetime of a local.
Looking at this github issue, I find the pair is relevant to llvm.lifetime.start / llvm.lifetime.end (which the pair is directly translated into when rustc lowers MIR to LLVM IR, perhaps?) .
Besides their semantics, I wonder what is the original design goal of the pair?
One of the reason I can think of is that we reuse LLVM's llvm.lifetime.start / llvm.lifetime.end to reduce the overall stack space intra-procedurally. Are there any other reasons?
I'd like to hear about the latest status of StorageLive / StorageDead.
I'd also like to help, e.g., write up their design and implementation somewhere, since some valuable contents are outdated or scattered.
Peeking at the rustc source code might be a choice, but I kind of don't know where to start and sometimes I can get lost. It would be appreciated if someone could provide some mentoring or discussion.
Then I guess the construction of them is determined only by the lexical scope (inspired by this example) and is not exploiting any data flow analysis.
The current borrow checker (NLL) is non-lexical and is more powerful. So perhaps the construction of StorageLive / StorageDead can be optimized after borrowck? In the above example, the interval between the StorageLive / StorageDead of a1 and a3 can be shorter. But I am not sure whether the optimization is beneficial and worthwhile.
fn test(a: Option<u32>) -> u32 {
let x = match a {
Some(y) => y,
None => 4,
};
x + 1
}
The generated mir:
fn test(_1: Option<u32>) -> u32 {
debug a => _1; // in scope 0 at src/main.rs:5:9: 5:10
let mut _0: u32; // return place in scope 0 at src/main.rs:5:28: 5:31
let _2: u32; // in scope 0 at src/main.rs:6:9: 6:10
let mut _3: isize; // in scope 0 at src/main.rs:7:9: 7:16
let _4: u32; // in scope 0 at src/main.rs:7:14: 7:15
let mut _5: u32; // in scope 0 at src/main.rs:10:5: 10:6
scope 1 {
debug x => _2; // in scope 1 at src/main.rs:6:9: 6:10
}
scope 2 {
debug y => _4; // in scope 2 at src/main.rs:7:14: 7:15
}
bb0: {
StorageLive(_2); // scope 0 at src/main.rs:6:9: 6:10
_3 = discriminant(_1); // scope 0 at src/main.rs:6:19: 6:20
switchInt(move _3) -> [0_isize: bb1, 1_isize: bb3, otherwise: bb2]; // scope 0 at src/main.rs:6:13: 6:20
}
bb1: {
_2 = const 4_u32; // scope 0 at src/main.rs:8:17: 8:18
goto -> bb4; // scope 0 at src/main.rs:8:17: 8:18
}
bb2: {
unreachable; // scope 0 at src/main.rs:6:19: 6:20
}
bb3: {
StorageLive(_4); // scope 0 at src/main.rs:7:14: 7:15
_4 = ((_1 as Some).0: u32); // scope 0 at src/main.rs:7:14: 7:15
_2 = _4; // scope 2 at src/main.rs:7:20: 7:21
StorageDead(_4); // scope 0 at src/main.rs:7:20: 7:21
goto -> bb4; // scope 0 at src/main.rs:7:20: 7:21
}
bb4: {
StorageLive(_5); // scope 1 at src/main.rs:10:5: 10:6
_5 = _2; // scope 1 at src/main.rs:10:5: 10:6
_0 = Add(move _5, const 1_u32); // scope 1 at src/main.rs:10:5: 10:10
StorageDead(_5); // scope 1 at src/main.rs:10:9: 10:10
StorageDead(_2); // scope 0 at src/main.rs:11:1: 11:2
return; // scope 0 at src/main.rs:11:2: 11:2
}
}
There is no StorageLive(_0) too. Since _0 is return value, I guess the reason might be that the semantics/effect of StorageLive/StorageDead might be implicit for _0. Another reason might be that StorageLive/StorageDead are optional for a local.
By taking a look at a refined example's LLVM IR and LangRef, I find llvm.lifetime.start/llvm.lifetime.end are for stack memory objects allocated from alloca, and not for general virtual registers.
Function parameters and the result are alive for the entire function body, so I do not find it surprising that there is no StorageLive/StorageDead for them. However, temporary variables used to store discriminants are alive only for certain blocks of the function, so it is surprising that there is no StorageLive/StorageDead for them.