Another take on `ImplictDrop`

Summary

A trait for types whose values may not be dropped automatically and therefore require additional attention.

Motivation

  • Additional of this trait allows library authors to force users to think about concrete uses of certain values they are provided with.
  • The intension is to add absolutely minimal support for linear types.
  • Make working with such values not too complicated via mechanism.

Guide-level explanation

The trait is defined as follows:

pub auto trait ImplicitDrop {}

This is an auto trait, built in into the language.

It is implied bound for all generic parameters, like Sized.

The syntax which is used to opt out from it is following: ?ImplicitDrop.

The trait is here to allow types to require explicit disposing and thus making code more readable and safe.

Essentially this is hard #[must_use] lint on the type level. Supposed to be used in cases where accidental drop of a value may cause undesired behavior.

Example:

enum TransactionState {
  Clean,
  InProgress,
}

struct TransactionGuard<`c,const STATE: TransactionState>
{..}

impl<`c> !ImplicitDrop for TransactionGuard<`c,{TransactionState::InProgress}> {};

fn example() {
  //...
  let mut handle = handle.begin(); // yields "transaction in progress type";
  match data {
    Some(data) => {
      //...
      handle.commit(); //value consumed, all is ok.
    },
    None => {
      //...but here we also have to finalize transaction
      // if we comment the next line the code will fail to compile
      handle.abort();
    }
  }
}

Note, the trait on its own has nothing to do with finalization, etc. This means that desired ways of consuming value are determined separately, and most probably going to depend on concrete contexts, etc.

Finalizer methods

New attribute #[finalize] is introduced. It marks a method as final destination of a value. It requires an implementor type to be Drop and annotated method to take self by value. Drop type are allowed to be destructed only inside #[finalize] methods. These methods can have return types.

impl<`c> TransactionGuard<`c,TransactionState::InProgress> {
  #[finalize]
  fn commit(self) -> Result<(),Error> {...};

  #[finalize]
  fn abort(self) {...};
}

Important detail is that if finalizer methods is not in scope (and not inherent), it's not required to be called, instead drop is considered as the least possible finalization method. The main reason of doing so is that we don't run in scoping problems and avoid concerns about the default finalization way.

Reference-level explanation

pub auto trait ImplicitDrop {}

The opt out bound ?ImplicitDrop disables calling a destructor on leaving a scope for values of some bound type. Such value going out of scope results in an error.

So the following will not compile:

fn impl_show<T: ?ImplicitDrop>(arg: T) {
  say_hello();
  //error: cannot implicitly drop function's argument
  //suggestion: add `drop(arg);`
}

The reason of doing the trait in such way is that this will require no changes in existing code:

  • All types in the language will automatically get it implemented;
  • It's opt-out bound like Sized.
  • It requires adding support for such types on per container basis. But in most cases one is already calling drop (or drop_in_place) manually, or adding such a support is one line level change (thanks to drop).

Finalization

We add #[finalize] attribute for methods.

It allows to destructure the type regardless of whether it implements Drop or not.

Using this attribute implies and requires Self to be !ImplicitDrop.

Implementation

There are two approaches on how to implement this:

  1. We compile code as usual, but when MIR tries to insert drop call for a !ImplicitDrop value we error out.
  2. We do CFG (dataflow) analysis, during it we determine which values go out of scope and check whether we can implicitly drop any.

Drawbacks

The worst thing about this proposal is that it requires library support for !ImplicitDrop types.

Rationale and alternatives

  • I've considered other variants for finalization: however, making #[finalize] to actually implement a trait leads to:
    • Need to include these traits in prelude => edition
    • Fn-like traits => burden
  • Also, there are multiple approaches to adding linear types:
    • Make T: Drop to actually make some sense => not backward compatible
    • Forget auto-trait on its own => only opens a way for linear types, but doesn't assist in using them.

Prior art

Unresolved questions

  • The merit of the proposal.

Is a struct containing a !ImplicitDrop field automatically !ImplicitDrop?

Yes, it's an auto-trait