---
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)