One of the borrow checker's major limitations is that it's not possible to create a reference that borrows from a subset of a type (such that borrows of non-overlapping subsets will not conflict). A langauge-level solution to this problem has long been desired.
For example, given a struct Foo { x: i32, y: String, z: u32 }
, here's a
(non-exhaustive) list of things people might want to express:
- A reference to a
Foo
takes a shared borrow of fieldsx
andy
only, both for lifetime'a
- A reference to a
Foo
that takes a shared borrow of fieldx
and a mutable borrow of fieldz
, both for lifetime'a
- A reference to a
Foo
that takes a shared borrow of fieldx
for lifetime'a
, and a mutable borrow of fieldy
for lifetime'b
Prior discussion:
- @nikomatsakis's blog post that started it all, and its comment thread. I would suggest reading that post (if you haven't already done so) before continuing with this one.
- https://blog.yoshuawuyts.com/safe-pin-projections-through-view-types/ and its comment thread
- https://internals.rust-lang.org/t/view-types-give-borrow-checker-more-information-about-your-code/16665
- https://internals.rust-lang.org/t/view-types-based-on-pattern-matching/16879
- Probably plenty more I'm missing
There are two potential approaches here. Either the referent (T
in
&T
/&mut T
) specifies what can be accessed—usually called "view types"; or
the referee (the &
/&mut
itself) does—I'll call this "reference views".
Approach 1: View types
Niko Mastakis's blog post takes the first approach, with the following justification:
When I’ve thought about this problem in the past, I’ve usually imagined that the list of “fields that may be accessed” would be attached to the reference. But that’s a bit odd, because a reference type
&mut T
doesn’t itself have an fields. The fields come fromT
.
However, this model has several issues:
- Mutability gets specified twice, as part of both the reference and the view.
- This inconvenience is balanced by the advantage of being able to specify mutability restrictions for owned values; but RFC 3323 likely makes that feature redundant.
- It's impossible to have a reference that borrows multiple fields for different lifetimes.
- How should owned view types be treated from an opsem perspective? Can they be
memcpy
ed? Transmuted to[MaybeUninit<u8>]
? Stored in aBox
?mem::swap
ped?drop()
ped? etc.
For these reasons, I believe that Niko's original instincts were correct, and putting the view on the reference is the right choice.
Approach 2: Reference views
Placeholder syntax
What might "reference views" look like? The &'a T
/&'a mut T
family would be
extended with many more access modes. I won't make a full syntax proposal in
this post. But for the sake of examples, this is the notation I'll be using,
given the definition of Foo
from earlier:
Field x |
Field y |
Field z |
Reference type |
---|---|---|---|
Shared borrow for 'a |
Shared borrow for 'a |
Not borrowed | &<'a x, 'a y> Foo |
Shared borrow for 'a |
Not borrowed | Mutable borrow for 'a |
&<'a x, 'a mut z> Foo |
Not borrowed | Mutable borrow for 'a |
Mutable borrow for 'b |
&<'a mut y, 'b mut z> Foo |
&'a Foo
is equivalent to &<'a x, 'a y, 'a z> Foo
; similarly, &'a mut Foo
is equivalent to &<'a mut x, 'a mut y, 'a mut z> Foo
. (Or are they? See
"Aliasing model" section.) &<> Foo
is a reference with access to nothing.
Subtyping implications
For view references to be usable, a function that accepts them should be strictly more flexible than one that accepts the correspoding non-view reference. For example:
-
fn foo<'a>(_: &<'a x> Foo)
is more flexible thanfn foo<'a>(_: &<'a x, 'a y> Foo)
is more flexible thanfn foo<'a>(_: &'a Foo)
. -
fn foo<'a>(_: &<'a mut z> Foo)
is more flexible thanfn foo<'a>(_: &<'a mut y, 'a mut z> Foo)
is more flexible thanfn foo<'a>(_: &'a mut Foo)
.
Function signatures are contravariant in their inputs, so the above implies that:
&<'a x> Foo
is a supertype of&<'a x, 'a y> Foo
is a supertype of&'a Foo
.&<'a mut x> Foo
is a supertype of&<'a mut x, 'a mut y> Foo
is a supertype of&'a mut Foo
.
How does this extend to views with both shared and mutable portions? First of all, views compose:
fn foo<'a>(_: &<'a x> Foo)
andfn foo<'a>(_: &<'a mut y> Foo)
are more flexible thanfn foo<'a>(_: &<'a x, 'a mut y> Foo)
.
Therefore,
&<'a x> Foo
and&<'a mut y> Foo
are supertypes of&<'a x, 'a mut y> Foo
.
But, what's the relationship between &<'a x> Foo
and &<'a mut x> Foo
? Well,
- Logically,
fn foo<'a>(_: &<'a x> Foo)
should be more flexible thanfn foo<'a>(_: &<'a mut x> Foo)
, right? - So,
&<'a x> Foo
is a supertype of&<'a mut x> Foo
. - Therefore,
&<'a x, 'a y, 'a z> Foo
is a supertype of&<'a mut x, 'a mut y, 'a mut z> Foo
. - Therefore,
&'a Foo
is a supertype of&'a mut Foo
. - Therefore,
&'b &'a Foo
is a supertype of&'b &'a mut Foo
. - But that would be unsound.
Therefore, either substituting fn foo<'a>(_: &<'a mut x> Foo)
with
fn foo<'a>(_: &<'a x> Foo)
must be forbidden, or the variance rules must give
view mutability special treatment.
Monomorphization and TypeId
Under this scheme, &'a T
and &'a mut T
are equivalent modulo views. Yet they
also have different TypeId
s. Therefore, we must say that references that
differ in views have distinct TypeId
s and are monomorphized separately.
Multiple-lifetime views
Consider the following trait, and its implementation for Foo
:
trait Frob {
fn frob<'a>(&'a mut self) -> &'a mut u32;
}
impl Frob for Foo {
fn frob<'a>(&'a mut self) -> &'a mut u32 {
*self.x += 1;
&mut self.z
}
}
As written, Foo::frob
's signature requires that self
remain mutably
borrowed, and therefore inaccessible, in its entirety for as long as the
returend &mut u32
is live.
fn test(f: Foo) {
let mut_u32 = f.frob();
dbg!(&f.x); // ERROR: f is mutably borrowed
dbg!(mut_u32);
}
This is needlessly restrictive, however: the
returned mutable borrow only involves the z
field. The Frob
implementation
could be updated to use a view reference to reflect this:
impl Frob for Foo {
fn frob<'a: 'b, 'b>(&<'b mut x, 'a mut z> self) -> &'a mut u32 {
*self.x += 1;
&mut self.z
}
}
In the updated signature, the returned &mut
only references 'a
; self.x
was
borrowed for 'b
, not 'a
, so it can be borrowed again as soon as frob
returns, even while the &'a mut u32
is still live.
fn test(f: Foo) {
let mut_u32 = f.frob();
dbg!(&f.x); // works! only `f.z` is still borrowed.
dbg!(mut_u32);
}
However, the new signature introduced a new lifetime parameter 'b
to frob
's
signature. As long as the parameter is late-bound (as it is in this example),
this transformation is at most a minor breaking change (might break turbofish);
but if it's early-bound, it could be major breakage (I think?).
Aliasing model
A view reference like &<x, mut z> Foo
might have access to several
discontiguous regions of memory, and there could be another reference like
&<y> Foo
concurrently accessing memory in the "gap" between x
and z
. The
Rust aliasing model, and the annotations that Rust emits to LLVM, would need to
reflect this.
One question that would need an answer is which view references are allowed to read or write padding bytes. There are several options here:
- View references have no access to padding, only full
&T
and&mut T
do. (This means that&T
/&mut T
are no longer equivalent types to a view over all fields.) - Only a view over all fields has access to padding, and this access is mutable only if all the views are mutable-access.
- A reference with view of two fields separated only by padding has view over that padding, and that view if mutable iff the view to both fields is mutable. Access to padding at the beginning or end of a struct is granted by view over its first or last field (as laid out in memory) respectively.
- References can access the padding immediately following each field they can view (with the corresponding mutability); access to padding before the first field is controlled by access to the first field.
- Etc…
Conclusion
I have no plans to develop this into a full proposal, but I hope these notes are helpful to anyone else who decides to do so. I would appreciate people's thoughts, especially on view types versus reference views, mutability subtyping, and aliasing-model questions.