Projections that do not decide mutability up front are possible

⚓ Rust    📅 2025-07-13    👤 surdeus    👁️ 3      

surdeus

I encountered the following problem while working on a transpiler from non-Rust code to Rust. Suppose that I have these structs:

#[derive(Debug, bytemuck::TransparentWrapper)]
#[repr(transparent)]
struct Foo(pub i32);

struct Bar {
    pub x: i32,
    pub y: i32,
}

Suppose that I want to pretend that Bar has fields of type Foo as much as it has fields of type i32, that is, be able to write an expression that:

  • is a place expression,
  • denotes the field x or y of a Bar, in the same way that bar.x and bar.y do,
  • has the type Foo, not i32 (which is sound because of the repr(transparent)), and
  • can be borrowed as mutable or shared without deciding while writing the expression.

After presuming for a while that this is impossible (after all, conventional Rust APIs have all these separate *_ref() and *_mut() functions, which must be there for a reason), I realized today that there is in fact an overloadable operator in Rust that offers this opportunity: the indexing operator.

use std::ops;
use bytemuck::TransparentWrapper;

struct X;
struct Y;
impl ops::Index<X> for Bar {
    type Output = Foo;
    fn index(&self, X: X) -> &Self::Output {
        TransparentWrapper::wrap_ref(&self.x)
    }
}
impl ops::Index<Y> for Bar {
    type Output = Foo;
    fn index(&self, Y: Y) -> &Self::Output {
        TransparentWrapper::wrap_ref(&self.y)
    }
}
impl ops::IndexMut<X> for Bar {
    fn index_mut(&mut self, X: X) -> &mut Self::Output {
        TransparentWrapper::wrap_mut(&mut self.x)
    }
}
impl ops::IndexMut<Y> for Bar {
    fn index_mut(&mut self, Y: Y) -> &mut Self::Output {
        TransparentWrapper::wrap_mut(&mut self.y)
    }
}

These definitions let this sort of code compile:

fn simple_example() {
    let mut bar = Bar { x: 0, y: 0 };
    bar[X] = Foo(1);
    bar[Y] = Foo(2);
    println!("{:?} {:?}", bar[X], bar[Y]);
}

fn more_specific_borrow_checked_usage(input: &mut Bar) {
    // Can have multiple borrows (must not use `&mut input`)
    let x1 = &input[X];
    let x2 = &input[X];
    println!("{x1:?}, {x2:?}");
    // Can mutate (must not use `&input`)
    let x3 = &mut input[X];
    *x3 = Foo(10);
    assert_eq!(input.x, 10);
}

It will even work on foreign types:

impl ops::Index<X> for [i32; 2] {
    type Output = Foo;
    fn index(&self, X: X) -> &Self::Output {
        TransparentWrapper::wrap_ref(&self[0])
    }
}
// ...

I haven't yet used this trick (I only just thought of it while I was cleaning out some old draft posts) and I may or may not actually use it, but I thought it was worth sharing.

2 posts - 1 participant

Read full topic

🏷️ rust_feed