Skip to content

MCP: Allowing the compiler to eagerly drop values #86

@djc

Description

@djc

Proposal

Summary and problem statement

Allow the compiler to drop values (when they are "dead" in the control flow graph liveness sense) before the end of the lexical scope in order to make Rust more ergonomic to write.

Motivation, use-cases, and solution sketches

There are a number of cases where the compiler currently requires more elaborate code than it perhaps should in order to be able to clean things up in the correct order.

1. impl Drop invalidates code

While refactoring Quinn, I ran into this issue:

fn simple() {
    let mut foo = Foo;
    let mut adapter = foo.simple();
    adapter.next();
    adapter.next();
    let mut adapter = foo.simple();
    adapter.next();
}

struct FooAdapter<'a>(&'a mut Foo);

impl<'a> FooAdapter<'a> {
    fn next(&mut self) -> Option<usize> {
        Some(3)
    }
}

fn sophisticated() {
    let mut foo = Foo;
    let mut adapter = foo.sophisticated();
    adapter.next();
    adapter.next();
    let mut adapter = foo.sophisticated();
    adapter.next();
}

struct FooAdapterWithDrop<'a>(&'a mut Foo);

impl<'a> FooAdapterWithDrop<'a> {
    fn next(&mut self) -> Option<usize> {
        Some(5)
    }
}

impl<'a> Drop for FooAdapterWithDrop<'a> {
    fn drop(&mut self) {
        println!("droppy");
    }
}

struct Foo;

impl Foo {
    fn simple(&mut self) -> FooAdapter<'_> {
        FooAdapter(self)
    }
    
    fn sophisticated(&mut self) -> FooAdapterWithDrop<'_> {
        FooAdapterWithDrop(self)
    }
}

While simple() compiles, sophisticated() does not:

error[E0499]: cannot borrow `foo` as mutable more than once at a time
  --> src/lib.rs:24:23
   |
21 |     let mut adapter = foo.sophisticated();
   |                       --- first mutable borrow occurs here
...
24 |     let mut adapter = foo.sophisticated();
   |                       ^^^ second mutable borrow occurs here
25 |     adapter.next();
26 | }
   | - first borrow might be used here, when `adapter` is dropped and runs the `Drop` code for type `FooAdapterWithDrop`

I found it surprising that just implementing Drop for a type can cause downstream code to trivially fail to compile, for what I find to be no good reason: the first adapter is dead where the second one is instantiated, so I feel the compiler should allow this.

2. Dropping locks before await points

Here's a piece of code from bb8:

let (tx, rx) = oneshot::channel();
{
    let mut locked = self.inner.internals.lock();
    let approvals = locked.push_waiter(tx, &self.inner.statics);
    self.spawn_replenishing_approvals(approvals);
};

match timeout(self.inner.statics.connection_timeout, rx).await {
    Ok(Ok(mut guard)) => Ok(PooledConnection::new(self, guard.extract())),
    _ => Err(RunError::TimedOut),
}

I think the inner lexical scope there should not be necessary. The compiler should know to drop locked before the timeout().await.

Possible solutions

An ideal solution might be that the compiler can reason about "pressure" to drop eagerly and will do so automatically when needed. However, this is likely not viable without an edition change, because it would drop locks and other guard types more eagerly to the point where previously working code can become incorrect.

Failing that, a more compatible solution might be to have an opt-in EagerDrop marker trait that can be implemented by types that want to opt in to to having their destructors run more eagerly when viable.

Prioritization

I think this is aligned with the lang team priority to improve borrow checker expressiveness and other lifetimes issues. It might also indirectly help with async code insofar as await points can apply "pressure" to drop more eagerly, making the use of locks and other Drop implementers in async code easier.

Links and related work

I'm not aware of any particular related work, although I think lock guards in async code come up frequently in support forums.

Initial people involved

So far, I have informally discussed this with @nikomatsakis, who recommended I submit this project proposal.

What happens now?

This issue is part of the experimental MCP process described in RFC 2936. Once this issue is filed, a Zulip topic will be opened for discussion, and the lang-team will review open MCPs in its weekly triage meetings. You should receive feedback within a week or two.

This issue is not meant to be used for technical discussion. There is a Zulip stream for that. Use this issue to leave procedural comments, such as volunteering to review, indicating that you second the proposal (or third, etc), or raising a concern that you would like to be addressed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions