[Idea] Attributes to warn if value is dropped without being acted upon

Note that if manually closing the handle (e.g., because it requires some additional parameter) is paramount, then the code can be refactored into CPS with a type-level “proof-of-work” to ensure the user does close it (unless the code panics):

/// instead of:
#[cfg(any())]
mod lib {
    pub
    struct Handle (());
    
    impl Handle {
        pub
        fn new () -> Self
        {
            Self(())
        }

        pub
        fn close (self, some_param: i32)
        {
            println!("Handle was correctly closed with {}", some_param);
        }
    }
}

/// do this
mod lib {
    pub
    struct Handle (());
    
    pub
    struct ClosedHandle (());
    
    impl Handle {
        pub(self) // private
        fn new () -> Self
        {
            Self(())
        }

        pub
        fn with_new (f: impl FnOnce(Handle) -> ClosedHandle)
        {
            f(Self::new());
        }
        
        pub fn close (self, some_param: i32) -> ClosedHandle
        {
            println!("Handle was correctly closed with {}", some_param);
            ClosedHandle(())
        }
    }
}

fn main ()
{
    use lib::Handle;
    
    Handle::with_new(|handle| {
        handle.close(42)
    });
}

If the .close(42) line is uncommented, we get:

error[E0308]: mismatched types
  --> src/main.rs:55:31
   |
55 |       Handle::with_new(|handle| {
   |  _______________________________^
56 | |         // handle.close(42)
57 | |     });
   | |_____^ expected struct `lib::ClosedHandle`, found ()
   |
   = note: expected type `lib::ClosedHandle`
              found type `()`

It does become more cumbersome to use, but then again, there cannot be that many things whose closing logic cannot be covered by Drop.

4 Likes

It may involve the network if the file is on a networked file system (NFS, SMB, etc.), or may even have arbitrary calls back to userspace for FUSE mounts. The semantics of close errors are also a mess.

2 Likes

Well, I started this thread with outlining two cases where RAII in Rust is insufficient in its current state, so I expected the discussion to be more constructive than “Rust RAII is perfect and these usage patterns have to be reformed in unspecified ways to stop causing offense”.

Proof-of-consumption closures are even more cumbersome than strictly enforced linear types would be, because this makes embedding these values impossible without reworking container APIs in the same style.

I was recently playing with Clang's consumed annotations for C++. The consumed annotations go beyond linear types and are actually a basic typestate mechanism. There are three predefined states (unconsumed, consumed, and unknown); you can add an attribute to any argument of any function to specify what state the argument should be in, and (optionally) a different state the function leaves it in when it returns. But I'm mostly just using it to implement the equivalent of must_consume, by marking a class's destructor as requiring this to be in consumed state.

So far the implementation is pretty rough – I had to patch Clang myself to fix bugs with misidentified destructor calls. But I was only motivated enough to write those patches because it seems quite useful: an alternative to implicit destructors that works when the 'destructor' needs context argument(s). I'd love to see similar functionality in Rust...

2 Likes