--- title: "Meetup 2024: Linear types" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-09-10 url: https://hackmd.io/W50y43HnRQifRNeoGcVUag --- # Meetup 2024: Linear types ## what we think we want ("more or less") * types you can create and which you must move * one way to move is to deconstruct into field values * another common way is to call a `self` method (which can in turn deconstruct) one thing to clarify: async drop would desugar in, Niko's opinion ## what tyler wants * scoped async tasks * better io-uring, DMA, and similar APIs * APIs where you have to decide how to dispose (e.g., detach or cancel) * async drop probably ## What josh wants - transactions that must be either committed or rolled back, but can't be dropped - scoped async tasks if we get those for free along the way ## what niko wants tyler + drop-with-parameters + guarantee-stuff-will-execute (e.g. requiring initialization for direct memory access (DMA)) oh and btw it's annoying to have to do `std::mem::take()` in `Drop` ## must-move In the below, the distinction between "Destruct" and "Drop" is meant to be that Destruct represents a failure to move/dispose of the value on the non-unwinding return path (while `Drop` encodes the more general case of a destructor that is run for both unwind and non-unwind paths). (See further discussion regarding panics in sections below.) ```rust! mod dma { pub struct DmaHandle { f: DmaHandleInner } // MustMove opts out of drop glue (aka !Destruct). impl MustMove for DmaHandle { } impl DmaHandle { pub fn dispose(self) { let DmaHandle { f } = self; operating_system.cancel_request(f); } } } fn foo() { let dma = DmaHandle::new(); // ERROR: dma not moved } fn foo() { let dma = DmaHandle::new(); dma.dispose(); } fn generic<T>(t: T) { // `T: Drop` } fn baz() { generic(DmaHandle::new()); // ERROR: T does not implement Destruct } fn generic<T: MustMove>(t: T) { // error } fn passthrough<T: MustMove>(t: T) -> T { t } trait Dispose: MustMove { fn dispose(self); } fn fails_to_dispose<T: Dispose>(t: T) { // ERROR: T does not implement Destruct } fn dispose<T: Dispose>(t: T) { t.dispose(); } ``` ## what will be annoying? ```rust // by itself can't really use MustMove with things like this trait Iterator { type Item; } impl<T> Iterator for Vec<T> { // requires: T: Destruct type Item = T; } ``` ### how does you get async-drop? key point is that MustMove will ensure you never pass an async-drop value into a sync fn that then drops it; the MustMove constraint will keep that from happening. sugar target for async-drop ### control-flow operations `?`, `break`, `return` partial answer: do-finally, defer ### panics what do you do with a panic? abort? lint, suggest defer? can decide per type? customize it? ### etc ## use patterns * ABSOLUTELY must call * DMA that is not cancellable * Absolutely must call but can recover * * Must call on happy path (but on panic, not so important then) * Database transaction * Structured concurrency (need to explicitly detach or cancel so there are no surprises in the happy path) * Decrement counter (but datastructure is not unwind-safe; recovery on panic is a no-op) * Can drop * Force: ```rust #[non_exhaustive] pub struct CallEnd { } impl MustMove for CallEnd { } pub struct Context { } impl Context { fn begin(&mut self) -> CallEnd { CallEnd { } } fn end(&mut self, r: CallEnd) { let CallEnd {} = r; // ... } } ``` ## edge cases and things that are hard ### panic c.f. boat's post about [try-finally](https://without.boats/blog/asynchronous-clean-up/) ### backcompat for associated types Solutions... * Can have trait transformer that relaxes these implied bounds, like `MustMove Iterator` * Can do the same thing as `Pin` ```rust! trait Deref { type Target: ?Sized; fn deref(&self) -> &Self::Target; } trait DerefMut: Deref { fn deref_mut(&mut self) -> &mut Self::Target; } // this would break if we changed // Target to be MustMove (grr) fn compare<D>(boxed: D, other: D::Target) -> bool where D: Deref, // ERROR no `D::Target: MustMove` bound { *boxed == other } ``` ```rust fn swap_ref<R: DerefMut>(a: R, b: R) { std::mem::swap(&mut *a, &mut *b) } ``` Digression: Could effects provide an escape hatch to get us out of the above morass with `Deref::Target` implicitly being `!MustMove`: What does `MustMove Deref` mean? ```rust trait MustMove Deref { type Target: ?Sized + MustMove; ... } ``` Associated effects/bounds mapping: ```rust trait Deref { bound E = MayMove; // Default effect. type Target: ?Sized + Self::E; ... } struct Droppable; impl Deref for Droppable { type Target = Droppable; ... } struct NotDroppable; impl MustMove for NotDroppable; impl Deref for NotDroppable { bound E = MustMove; type Target = NotDroppable; ... } ``` ```rust trait Deref { trait TargetBound = MayMove; // <-- associated trait aliases are "easy-ish" type Target: ?Sized + Self::TargetBound; } fn foo<trait T>() { // ^^^^^^^ these are awfully hard, have to // infer values for them, // pick impls based on them, // etc } fn some_generic_code< D: Deref<trait TargetBound = MustMove> >(d: Box<D>) { let value = d.into_inner(); // ERROR: must move value } ``` ```rust pub enum Option<T: MustMove> { Some(T), None } impl<T: MustMove> Option<T> { pub fn map<U: MustMove>( self, value: impl FnOnce(T) -> U, ) -> Option<U> { match self { Some(t) => Some(value(t)), None, } } } fn something<X: MustMove>() { let v: Option<X> = None; } ``` #### sketching out a pointer-as-proof mechanism ```rust let x: Transaction = get_must_move() defer { x.dispose() }; let r: &move mut Transaction = &mut x; impl Transaction { pub fn } ``` ### working out async drop ### unforgettable ```rust trait Unforgettable /*!Leak*/ {} ``` ^ May want `Unleak<T>`: https://zetanumbers.github.io/book/myosotis.html#the-unleak-wrapper-type * can't use with `forget` * can't put into an `Rc` or `Arc` (because cycles) advantages: * "less" (achieves fewer goals, notably not async-drop or transactions) disadvantages: * "less" (does less stuff)