Why drop flags?

I thought that the places where a variable is dropped should have been statically known until I read this page: https://doc.rust-lang.org/nomicon/drop-flags.html.

It seems strange to me, since theoretically it should be possible to statically track whether a variable could exist. If there is any possibility that a variable is uninitialized at one point, the variable could be dropped just before that point since that variable cannot be used anyway.

For the following code:

fn main() {
    let mut x: Box<u64>;
    if true {
        x = Box::new(0);
    }
    // Point 1
    x = Box::new(0);
}

It looks like the compiler is already doing such an analysis, since attempting to execute drop(x); at Point 1 gives error E0381: use of possibly-uninitialized variable. Why not drop variables just before the point where it is possibly uninitialized?

This is an interesting idea, and at least to me, it seems feasible to design it this way.
One disadvantage would be that the order in which things are dropped would depend on later uses. This might lead to surprising, subtle behaviour changes. This is also the main reason we can't introduce this now: currently, the drop order in Rust is clearly defined, so we are not free to drop earlier without breaking backwards compatibility.

1 Like

I searched for relevant documents but the drop order seems to only be defined for tuples, structs, slices and Vec. I'm not aware of any documents that define in which order local variables should be dropped. To avoid breaking compatibility, this can be an opt-in, and in case the user wants to explicitly specify the drop order, they can call drop() manually anyway.

I'm not sure what you're suggesting? It's would be unsound to call drop on a variable which is uninitialised, so I don't see how introducing extra drop calls could possibly help, or indeed what problem you're trying to solve?

Note also: drop flags are purely a description of how this might be implemented in the compiler, there's no obligation to actually use anything resembling boolean flags in the compiler, and the way this is implemented has no observable effects on a rust program.

1 Like

When a variable is possibly-uninitialized, you can’t access it directly, but you can potentially access it through references that were taken at a point where it was known to be initialized. Example:

fn main() {
    let mut x: Box<u64>;
    let mut y: &u64 = &42;
    if true {
        x = Box::new(0);
        y = &x;
    }
    // Point 1
    println!("{}", *y);
    x = Box::new(0);
}
11 Likes

Fun fact: In older versions of Rust, every type that implemented Drop would contain a drop flag (modulo optimizations). Today, afaik rustc does track this statically whenever possible, and only resorts to drop flags on the stack when it must. Apparently making that feasible was one of the benefits of introducing MIR.

3 Likes

That makes sense. Thanks for pointing it out!

The drop was not actually to drop it, but to trigger a compiler error complaining that the variable is possibly uninitialized. This proves that the compiler does understand whether at each point of execution, a variable was guaranteed to exist or it may be uninitialized.

I'm not solving any specific problem; it is just kind of irritating because dynamic drop flags are a bit ugly in my own opinion (please forget about it). I had expected that Rust was smart enough to track moves statically rather than dynamically, and drops should happen as early as possible. That turns out to be a misunderstanding of the language though.

1 Like

AFAIK Rust is smart enough to track moves statically whenever possible (and where it can't, I assume LLVM will optimize out redundant drop flags).

That's different from semantics of Drop offered by the language. Rust intentionally guarantees things will be dropped at the end of scope, in a specific order. This allows code such as:

let _lock = lock();
use_stuff_under_a_lock();
// lock is dropped here

With eager drop this would be invalid: (let _ is a special syntax that drops immediately)

let _ = lock();
// lock is dropped here
use_stuff_under_a_lock(); // oops!
1 Like

This alternative (and more) was considered during the move to the current approach. For example:

4 Likes

This is not really relevant, since there are some arguments that are more compelling than mine. But for future reference: local variables are dropped in reverse order of declaration. This is documented in https://doc.rust-lang.org/std/ops/trait.Drop.html#variables-are-dropped-in-reverse-order-of-declaration.