View types experiment
| Metadata | |
|---|---|
| Point of contact | @scrabsha |
| Status | Proposed |
| Tracking issue | |
| Zulip channel | TODO |
| compiler champion | Oliver Scherer |
| Teams | compiler, lang, types |
| Task owners | @scrabsha |
Summary
Add view types support to rustc as well as a few syntaxes proposed by project
members for experimentation purposes.
Motivation
Rust currently does not allow tracking borrows across function calls: when a reference to an object is passed to a function, the borrow checker considers the entire object to be used by the function. Developers work around this by passing individual fields of a struct as arguments instead of passing the whole struct.
View types solve this by adding type-level annotations that restrict which fields of a struct can be accessed in a function body. The borrow checker allows other fields to be borrowed at the same time on the caller’s side.
The ideal syntax for view types is still debated. We aim to implement a few of the competing syntaxes so that people can experiment with them and form meaningful opinions.
The status quo
As stated in the previous section, the borrow checker cannot reason about individual fields of a struct being used or not by a specific function. It ends up rejecting code that would respect borrow-checking invariants, had the function call been inlined. Let’s consider the following code as an example:
#![allow(unused)]
fn main() {
struct Foo {
a: usize,
b: u32,
}
let mut foo = Foo { a: 101, b: 42 };
let a = &mut foo.a;
let b = &mut foo.b; // (1)
*b += 1; // (2)
*a += 1;
}
(Playground link)
This code compiles, let’s try extracting the statements (1) and (2) to their own function:
#![allow(unused)]
fn main() {
struct Foo {
a: usize,
b: u32,
}
fn increment_b(foo: &mut Foo) {
let b = &mut foo.b;
*b += 1;
}
let mut foo = Foo { a: 101, b: 42 };
let a = &mut foo.a;
increment_b(&mut foo);
*a += 1;
}
(Playground link)
This fails to compile. rustc version 1.96.0 emits the following error message:
error[E0499]: cannot borrow `foo` as mutable more than once at a time
--> src/main.rs:15:13
|
14 | let a = &mut foo.a;
| ---------- first mutable borrow occurs here
15 | increment_b(&mut foo);
| ^^^^^^^^ second mutable borrow occurs here
16 | *a += 1;
| ------- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
People generally fix this by making sure increment_b does not borrow “too much
data” and pass only the fields of Foo that the function really needs:
#![allow(unused)]
fn main() {
struct Foo {
a: usize,
b: u32,
}
fn increment_b(b: &mut u32) {
*b += 1;
}
let mut foo = Foo { a: 101, b: 42 };
let a = &mut foo.a;
increment_b(&mut foo.b);
*a += 1;
}
(Playground link)
This has the following drawbacks:
- This creates unnecessary friction during refactorings
- This requires adding one argument for each field that is used (instead of a single argument for the whole struct)
- IDEs do not support this, they generate code that does not compile
- Methods have to be turned into associated functions or free functions, which sometimes makes the code harder to understand
What we propose to do about it
General solution
We introduce view types: a zero-cost wrapper over an actual type that restricts the set of fields that can be accessed.
In the previous example, view types would make the code clearer:
#![allow(unused)]
fn main() {
struct Foo {
a: usize,
b: u32,
}
fn increment_b(foo: &mut Foo.{ b }) {
let b = &mut foo.b;
*b += 1;
}
let mut foo = Foo { a: 101, b: 42 };
let a = &mut foo.a;
increment_b(&mut foo);
*a += 1;
}
Three things happen here:
increment_btakes an&mut Foo.{ b }: a mutable reference to a value of typeFooof which only the fieldbcan be accessed- The
&mut foothat is passed toincrement_bis coerced to&mut Foo.{ b } - The borrow checker accepts this code because it now understands that
&mut foo as &mut Foo.{ b }only requires mutably borrowingfoo.b.
The Contiguous marker trait
Passing a view type as argument to memcpy is unsound:
#![allow(unused)]
fn main() {
fn copy<T>(src: T, dst: &mut T) {
unsafe extern "C" {
fn memcpy(n: usize, dest: *mut c_void, src: *const c_void);
}
unsafe {
memcpy(
size_of::<T>(),
&raw mut *dst as *mut c_void,
&raw const src as *const c_void,
);
}
}
}
The Contiguous marker trait is implemented for any type that is not a view
type. An implicit trait bound as also added (much like Sized currently). This
way, calling copy::<Foo.{}> would trigger a compilation error.
Limitations
This Project Goal aims to allow for experimentation around view types, not to allow people to use view types in production. As such, the following artificial limitations are introduced:
- Publicly exposing a view type in a crate’s API is a hard error, even if the
fields mentioned are
pub. - Interactions with pattern types are not considered.
Syntax
To facilitate testing, the following syntaxes will be added:
Ty.{ <fields> }syntax (from Maximally minimal view types, a follow-up)Ty.{ .. }as a shorthand forTy(from Maximally minimal view types)Ty.fieldas a shorthand forTy.{ field }(suggested by Benno Lossin at Rust All Hands 2026)- Disallowing
Ty.{ <fields> }in function prototypes, requiring them to be introduced via type aliases (akin to view groups) (suggested by Benno Lossin at Rust All Hands 2026) &mut { <fields> } Ty(from Syntactic Musings on View Types)
Work items over the next year
| Task | Owner(s) | Notes |
|---|---|---|
| First implementation of view types | @scrabsha | |
| Implementation of the alternative syntaxes | @scrabsha | |
Contiguous marker trait and implicit bound | @scrabsha |
Team asks
| Team | Support level | Notes |
|---|---|---|
| compiler | Medium | Code review, questions |
| lang | Small | Suggestion of alternative syntaxes |
| types | Small | Add the Contiguous marker trait and implied bound |
Frequently asked questions
(None?)