Crazy thought: what if we had first-class place expressions in the language?
&(mut)
currently enjoys a very special status. You can turn &struct
into &struct.field
, which is a map &'a Struct -> &'a Field
, but it is impossible to express that map in the language. If we wanted to make it a function, what would it even act on? Similarly, it's impossible to express its basic constructor t -> &t
as a function T -> &'a T
. It doesn't act on a value, it acts on a place expression.
This built-in magic behaviour is desirable for other smart pointers, but currently cannot be ergonomically and safely expressed in the language.
What if there was some opaque compiler-provided place expression object, which could be passed around in user code? A place expression to an object of type T
could implement some trait Place<T>
.
In its object form dyn Place<T>
it would basically be represented as a pointer *mut T
, with the distinction that we would know that it's in "dereferenced" form. In other words, place: *const dyn Place<T>
is just *const T
with the known type T
and no dyn metadata (similarly, place: *mut dyn Place<T>
is just *mut T
).
In the compile-time generic parameter form fn foo<P: Place<T>>(place: P, ..)
the place would carry compiler-provided metadata, which would allow a richer API. A place would know its provenance, so that we could check that e.g. field_place
is really a place to a field in struct_place
. Perhaps it should be possible to go from a field place to its parent struct place. This would allow to avoid dealing with explicit offsets of fields in structs in order to perform that operation.
A smart pointer type, e.g. Ref<Struct>
, would be able to provide the place of its contained data, i.e. a mapping impl Place<Ref<Struct>> -> impl Place<Struct>
(it would basically just return the dereferenced inner pointer, as a place). Given s: Place<Struct>
, we can get s.f: Place<Field>
, which can be passed into
Ref::restrict(self: impl Place<Ref<Struct>>, field: impl Place<Field>) -> impl Place<Ref<Field>>
The instance of Ref
can be recovered by converting place expression into a value expression, as usual. But if so desired, you could convert &Ref<Struct>
into &Ref<Field>
with the same function and a referencing.
Since the compiler has full information about both places, it can check that field
is really a place in the inner struct and not just some stray pointer. This means that the restriction function can be entirely safe. With a pointer-based or offset-based definition of restrict
, I just don't see a way to make a safe API.
Composability becomes easy. To turn Ref<MaybeUninit<Struct>>
into Ref<MaybeUninit<Field>>
, we first extract the place of the field via
Place<Ref<MaybeUninit<Struct>>> -> Place<MaybeUninit<Struct>> -> Place<Struct> -> Place<Field>
We get the required instance of Ref<MaybeUninit<Field>>
by spinning that process in reverse with restrict
functions, i.e.
MaybeUninit::restrict(impl Place<MaybeUninit<Struct>>, impl Place<Field>) -> impl Place<MaybeUninit<Field>>
Ref::restrict(impl Place<Ref<MaybeUninit<Struct>>>, impl Place<MaybeUninit<Field>>) -> impl Place<Ref<MaybeUninit<Field>>>
The last line is valid since we track the place provenance, and so we know that the given field: Place<MaybeUninit<Field>>
is really a place within struct: Place<MaybeUninit<Struct>>
. Also note that you can't turn impl Place<Struct>
into e.g. RefMut<Struct>
, since RefMut
doesn't provide a corresponding constructor, and the RefMut::restrict
method requires already having a valid RefMut
to restrict.
First-class place expressions would also be able to solve many (most? all?) cases of proliferation of get/get_mut
or iter/iter_mut
methods, which really don't care about the mutability, and can be essentially generated by a macro. With first-class place expression, we would have a function
fn get_place(self: impl Place<[T]>, n: usize) -> impl Place<T>
which computes the resulting place directly operating on the places of the slice (currently all operations differ only in pervasive const/mut
dichotomy). Similarly, we would have
fn iter_place(self: impl Place<[T]>) -> impl Iterator<Item=impl Place<T>>
and the end-user would choose the desired value iterator by applying the corresponding smart pointer constructor. It could be &T
or &mut T
iterators as usual, but also *const T
, Option<&T>
or even &MaybeUninit<T>
, if some unsafe trickery requires it.