>field projections

10~15min

the field projection project goal is about designing a language feature to make custom pointer types as ergonomic as the builtin reference types &T and &mut T. I’m working on this goal together with Tyler Mandry, Nadrieril, and Xiangfei Ding as well as many others on the Rust Zulip. this blog post gives a short introduction of the idea, shows what we’re currently working on, and offers links to more resources.

note: I wrote this post back in august and did not upload it until now. most of the information is still up to date, but you might find some minute differences to my current work.

published

the field projection project goal is about designing a language feature to make custom pointer types as ergonomic as the builtin reference types &T and &mut T. I’m working on this goal together with Tyler Mandry, Nadrieril, and Xiangfei Ding as well as many others on the Rust Zulip. this blog post gives a short introduction of the idea, shows what we’re currently working on, and offers links to more resources.

note: I wrote this post back in august and did not upload it until now. most of the information is still up to date, but you might find some minute differences to my current work.

# field projections – i.e. pointer ergonomics

the idea of field projections has persisted in my mind for a long time. it still is very relevant, but by now is no longer the main philosophy and motivation. instead, we’re trying to make all pointer types (smart, dumb, custom and builtin) more ergonomic.

# ergonomic pointers

the most ergonomic pointer type is &T (and &mut T). it has direct borrow-checker integration, one can access struct fields, and one can match enum variants. these last two properties are what field projections are. consider the following example struct:

struct Point {
    x: i32,
    y: i32,
}

we can now write the following:

let point: &Point = ...;
let field: &usize = &point.x;

this is a field projection, specifically, turning a pointer to a struct into a pointer to a field of the struct in an ergonomic fashion.

the same also happens when an enum is matched, the discriminant is read from the reference and then each field of each variant is projected:

let x: &Option<Point> = ...;
match x {
    Some(point) => {
        let _: &Point = point;
    }
    None => {}
}

# unergomonic pointers

sadly, not all pointer types have these nice ergonomics. however, Rust has been using a rather successful trick to port the ergonomics of references to custom pointer types: Deref and DerefMut. the deref[_mut] functions are called in several places implicitly by the compiler, for example allowing direct field access on custom pointer types.

however, Deref[Mut] have some pretty severe restrictions. we’ll take a look at the two most important ones now.

# no disjoint borrows with Deref[Mut]

Deref[Mut] gets no special treatment from the borrow checker and thus they cannot allow borrowing different fields at the same time:

let point: MyMut<'_, Point> = ...;
let x = &mut point.x;
let y = &mut point.y;

mem::swap(x, y);

the error the compiler gives is:

error[E0499]: cannot borrow `point` as mutable more than once at a time
 --> example.rs:5:14
  |
2 | let x = &mut point.x;
  |              ----- first mutable borrow occurs here
3 | let y = &mut point.y;
  |              ^^^^^ second mutable borrow occurs here
4 |
5 | std::mem::swap(x, y);
  |                - first borrow later used here

this might be confusing at first, but if we make the implicit calls of deref_mut explicit, the problem becomes apparent:

let point: MyMut<'_, Point> = ...;
let x = &mut DerefMut::deref_mut(&mut point).x;
let y = &mut DerefMut::deref_mut(&mut point).y;

mem::swap(x, y);

we’re creating two mutable reference through the two &mut point expressions, which we expect to use both in swap. if we instead write the code using &mut Point instead of MyMut<'_, Point>, then it compiles:

let point: MyMut<'_, Point> = ...;
let x = &mut point.x;
let y = &mut point.y;

mem::swap(x, y);

this works, because the dereference of &mut T is built-in and the borrow-checker understands that the fields are disjoint.

# having fewer guarantees than &[mut] T

the other major restriction of Deref[Mut] is that they cannot help with improving the ergonomics of pointers that have fewer guarantees than the reference types. for example: NonNull<T> – it cannot implement Deref, since it might not point at currently allocated memory. however, it would still benefit a lot from more ergonomic field projections, as doing a field projection currently looks like this:

let ptr: NonNull<Point> = ...;

let x: NonNull<usize> = unsafe {
    NonNull::new_unchecked(&raw mut (*ptr.as_ptr()).x)
};

instead of what one would want to write:

let ptr: NonNull<Point> = ...;

let x: NonNull<usize> = unsafe { &nonnull ptr.x };

# the current approach

the current approach combines many great ideas from several people. at the center of the proposal is the concept of places.

as part of the place operations that we’re enabling customization for, we have custom borrowing, which provides field projections for all:

let point: MyPtr<Point> = ...;
let x: MyPtr<usize> = @point.x;

the compiler desugars all place operations into a function calls using the operation trait associated with that place operation. in this particular case, the desugaring is:

let point: MyPtr<Point> = ...;
// this is the desugaring of `let field: MyPtr<Field> = @ptr.field;`
let x: MyPtr<usize> = unsafe {
    // this corresponds to the place that stores `point`.
    let hdl: LocalHandle<MyPtr<Point>> = LocalHandle::new(&raw const point);

    // there is an implicit dereference on `point` in order to access the
    // fields of the pointee:
    let hdl: MyPtrHandle<Point> = DerefPlace::deref(hdl);

    // accessing the field projects the pointer:
    let subplace = <field_of!(Point, x)>::default();
    let hdl: MyPtrHandle<usize> = ProjectPlace::project(hdl, subplace);

    // lastly, there is a borrow of the place that we computed:
    BorrowPlace::<MyPtr<usize>>::borrow(hdl)
};

we do not yet fully understood what the best strategy for choosing the return type of the custom borrow operator @ is. at the moment, we think that inferring it from the context might be a very powerful and most of the time correct choice. whether this works in practice and doesn’t result in lots of inference failures can only be verified when we have a compiler experiment.

regardless of what strategy we choose, one can always write @<$ty> $place_expr to give the type explicitly.

# ongoing work

at the moment, my exploration of this feature happens in this GitHub repository: https://github.com/BennoLossin/field-projections-designs. I’m writing sugared code (so code that takes advantage of the cool features we’re adding) and then I try to figure out how the compiler would go about desugaring it. With this, I hope to answer several design questions at the same time:

  1. are the ergonomic features that we provide actually useful and solve the motivational use-cases we have collected over the years?
  2. is there a sensible desugaring algorithm that we can hope to implement in the compiler?
  3. fow convenient/usable are the library additions and other parts of how the user controls the desugaring of the operations for their custom type?

# further resources

I already published another post about this feature, but since I never actually introduced this feature in that post, I thought I’d give a small section on reading material here: