- Feature Name:
substructural_traits - Start Date: 10-11-2025
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
Rust has parts of a substructural type system, but it is incomplete. A substructural type system is extremely important to correctly capture the behavior of real computer systems in the absence of garbage collection and presence of non-memory resources. As Rust has grown, the incomplete nature of Rust’s substructural type system has caused more and more problems, such as the Leakpocalypse, endless Rc cloning problems, and the scoped task trilemma. We can resolve many very thorny correctness and ergonomics problems throughout the language and library ecosystem by finishing the substructural features.
Motivation
Rust's incomplete implementation of substructural traits continues to cause problems in more and more cases. Leak/Forget is the most well-known, and pin!/Unpin is the consequence of not having a Move trait. However, there is still debate over whether Move should ever allow non-trivial move behavior, or if there needs to be a separate trait to handle it. Likewise, while Clone captures non-trivial copy behavior, there is now a proposal for "cheap" (or implicit) clones in the Ergonomic ref-counting proposal (possibly using a Handle trait). It seems Rust's efforts to fix the type system is unknowingly working towards a substructural type system. It doesn't seem like Rust realizes all these traits have corresponding substructural operations, as we can see in this table:
| Explicit | Implicit | Trivial | |
|---|---|---|---|
| Contraction | Clone | Handle | Copy |
| Weakening | Delete | Destruct | Leak/Forget |
| Exchange | Transfer | Relocate | Move |
A substructural type system provides ways to restrict three specific (and usually implicit) operations, known as Contraction, Weakening, and Exchange.
- Contraction means that if you can do an operation with two of something, you can also do it with one of that thing by using it for both, or in other words it’s possible to copy something.
- Weakening means that if you can do an operation without something, you can also do it with that thing, or in other words you can discard things you don’t need. This is the Destruct trait.
- Exchange means that if you can do an operation with an A and a B then you can also do it with a B and an A, or in other words you can rearrange and move things around.
As we can see from the table above, Rust has implemented traits for explicit Contraction, implicit Drop (as Destruct), and trivial Contraction. Current proposals are trying to add a concept of implicit Contraction (possibly as a Handle trait), and to create explicit marker traits for concepts the Compiler currently unconditionally assumes: trivial Weakening (in the form of a Forget trait) and trivial Move (which is currently worked around using the pin!/Unpin construction).
- Explicit operations require an explicit method call in the source code, and as such may do arbitrary expensive things that we would conventionally expect to be visible, for example cloning a large vec may have to do arbitrary amount of copies (or other computation) to handle every element of the vector.
- Implicit operations are ones that require custom code but only have to do a small constant amount of work, like copying an Rc which needs to only write to memory a small constant amount of data. (in my benchmarking it costs about as much as copying a struct with four fields on the stack).
- Trivial operations are ones that entirely elide into a standard memcpy, memmove, or memdrop, like copying primitives or structs of primitives.
Currently, Contraction is reified as the Copy-Clone hierarchy and implements Weakening with Destruct, although the current PR assumes it would only ever be useful in const contexts. Rust's only concept of Exchange is the compiler's assumption of all types having a trivial Move, which still has no proposal yet for being extracted into a marker trait.
If we were to abandon Rust's current naming scheme to more closely match each trait to their substructural equivalents, our table would look like this:
| Explicit | Implicit | Trivial | |
|---|---|---|---|
| Contraction | Clone | Copy | TrivialCopy |
| Weakening | Delete | Drop | TrivialDrop |
| Exchange | Transfer | Move | TrivialMove |
Such a renaming is likely impossible for backwards compatibility reasons, so we will be sticking with the existing traits, and the proposed traits Handle, Forget. To minimize confusion, we will use our theoretical Delete, Transfer, Relocate and Move traits, since most current discussions around a Move trait assume it represents a trivial move operation, although no move RFC has yet been proposed.
With this implementation, Rc becomes Handle and Move unconditionally, Destruct if the thing it contains is Destruct, and Delete if the thing it contains is Delete. Similarly, Box is Clone if the thing it contains is Clone, Handle if the thing it contains is Handle, Delete if the thing it contains is Delete, Destruct if the thing it contains is Destruct, and Move unconditionally.
pin! and Unpin are no longer needed for futures, which can simply not provide an Exchange implementation, by not implementing Transfer, Relocate, or Move. The Handle trait then makes using Arc/Rc smart pointers easier by providing a clear implicit operation.
Currently, Destruct allows performing arbitrary amounts of compute or IO at a region of the program that doesn’t even name the thing causing it, via a Drop implementation. By separating Delete from the implicit Destruct case, providing the Forget trait to mark the trivial case, and enforcing the Contraction requirement, it becomes much more clear when expensive operations are being used and when guard drop operations could be expensive. This solves the problem of guard types potentially being implicitly dropped instead of needing to be explicitly dropped - by only implementing Delete, a guard type for a lock would always have to be explicitly dropped by the user.
Guide-level explanation
We will skip examples from the current Handle and Forget proposals, which focus on Rc ergonomics and scoped APIs, respectively. It is well-known that an implementation of Move would allow simplifying async APIs:
impl<A, B> Future for Either<A, B>
where
A: Future,
B: Future<Output = A::Output>,
{
type Output = A::Output;
// No more `pin` needed
fn poll(self: &mut Self, cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
match self {
Either::A(x) => x.poll(cx),
Either::B(x) => x.poll(cx),
}
}
}
What's also interesting is that a non-trivial move actually allows moving self-referential types around, because now code can be run to re-assign the internal pointers (but this would require allowing users to construct self-referential data types in the first place, which is still up in the air).
struct ParseState {
data: String, // This should probably get some kind of marker that a borrow into the contents already exists, but that can be decided later
pos: &'self str, // points into `data` (using imaginary method of user-accessible self-referential structs)
}
impl !Move for ParseState {}
impl !Relocate for ParseState {}
impl Transfer for ParseState {
// Imaginary example of what an explicit move function might look like using a magic calling convention
extern "inline-self" fn transfer(self) -> Self {
let mut r: MaybeUninit::<Self>::uninit();
let offset: isize = unsafe { (self.pos as *const str).offset_from(&self.data as *const str) };
let ptr = r.as_mut_ptr();
unsafe {
std::ptr::addr_of_mut!((*ptr).data).write(self.data);
std::ptr::addr_of_mut!((*ptr).pos).write(&(&(*ptr).data)[offset as usize..]);
}
unsafe { r.assume_init() }
}
// Alternative imaginary example of what an explicit move function might look like using &own and &construct references with safe code
fn transfer(self: &own Self, target: &construct Self) {
let offset: isize = unsafe { (self.pos as *const str).offset_from(&self.data as *const str) };
std::mem::drop(self.pos);
std::mem::transfer(&own self.data, &construct target.data);
target.pos = &target.data[offset as usize..];
}
}
It is also possible to solve the implicit drop problem with guard types on locks, by implementing the explicit Delete trait (and opting out of the default Forget trait):
use std::ops::Deref;
struct Foo {}
struct Mutex<T> {
// We keep a reference to our data: T here.
}
struct MutexGuard<'a, T: 'a> {
data: &'a T,
}
// Locking the mutex is explicit.
impl<T> Mutex<T> {
fn lock(&self) -> MutexGuard<T> {
// Lock the underlying OS mutex and return a guard
MutexGuard {
data: self,
}
}
}
// Disallow implicitly dropping MutexGuard
impl !Destruct for MutexGuard {}
// Provide custom drop logic
impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
// Unlock the underlying OS mutex.
//..
}
}
// Implement Deref so we can treat MutexGuard like a pointer to T.
impl<'a, T> Deref for MutexGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
self.data
}
}
fn baz(x: Mutex<Foo>) {
{
let xx = x.lock();
xx.foo();
// Compiler error! Cannot implicitly drop MutexGuard when it goes out of scope!
}
{
let xx = x.lock();
xx.foo();
std::mem::delete(xx) // not using .delete() to avoid confusion due to Deref
// Compiles because xx is now deleted before going out of scope.
}
}
Making the ability to destroy something an explicit trait also allows us to create types that cannot be destroyed unless manually disassembled. Such types are, by definition, not UnwindSafe, so they can't be safely used in contexts where unwinding might occur. This is extremely useful for creating reservation types that track some underlying resource without needing to bundle an Rc pointer to it. Instead, forgetting to call a function that consumes the reservation type will always result in a compiler error.
struct Wrap(usize);
// Nonsensical usage of !Forget to provide an example of non-forgettable regions
impl !Forget for Wrap {}
struct Reserved {
id: Wrap,
pub area: f32,
}
// Disallow destroying Reserved (also implies !UnwindSafe, !Destruct, and !Forget)
impl !Delete for Reserved {}
struct TextureAtlas {
areas: HashMap<usize, f32>,
count: u64,
}
impl TextureAtlas {
pub fn reserve(&mut self, area: f32) -> Reserved {
let id = count;
count += 1;
self.areas.insert(id, area);
Reserved {
id: Wrap(id),
area,
}
}
pub fn release(&mut self, reservation: Reserved) {
// Move all !Forget elements out of reservation
let id = reservation.id;
let old = self.areas.remove(id.0).unwrap();
assert_eq(old, area);
// As long as all non-forgettable fields in `reservation` have been moved out of, this will compile. If we had missed any fields, this would fail to compile.
// Optionally, the compiler could also error on any forgettable fields that have custom drop logic.
}
}
More formally, uninitialized memory is trivially droppable, or Forgettable. A custom deletion can be accomplished by moving or deleting all non-Forget members of the type, leaving only uninitialized memory or Forgettable types. This is how &own and &construct connect to substructural typing, because they represent references to a region that must become uninitialized or become initialized before the end of the function. &construct can allow making self-referential structs by writing fields into the region one at a time and has the normal borrow checker guarantees of not being able to access an uninitialized field. &own facilitates custom destruction or consumption logic to operate in place.
Reference-level explanation
Some of the technical details of this RFC are covered by its prerequisites, like new auto-traits and Forget. However, if the move traits are implemented in a similar manner to Copy/Clone, this would introduce a unique auto-trait situation, covered below. This situation does not arise if custom move logic is implemented in the same way Drop is.
Just like how Copy currently implies Clone, the insertion of an implicit Handle trait would imply Copy: Handle and Handle: Clone. The other traits will follow a similar pattern:
| Explicit | Implicit | Trivial |
|---|---|---|
Clone |
Handle: Clone |
Copy: Handle |
Transfer |
Relocate: Transfer |
Move: Relocate |
Delete |
Destruct: Delete |
Forget: Destruct |
Note that UnwindSafe must also imply Delete. There may be other internal compiler traits with additional implications that are currently ignored becuase all traits must implement them anyway.
Move Trait Requirements
In order to satisfy the substructural trait hierarchy, the set of Move traits (Move, Relocate and Transfer) should mimic the trait inheritance pattern of the Clone traits. However, unlike Copy/Handle/Clone, Move is an auto-trait, and if Move implies Relocate and Relocate implies Transfer, then both Relocate and Transfer must therefore also be auto-traits. This by itself isn't a problem, it's the fact that Transfer must be an auto-trait despite not being a marker trait. There is a workaround discussed below that avoids this construction, but complicates the usage.
To handle the novel situation of a non-marker auto-trait, the trait could simply provide a default implementation of fn move() that is mapped to or corresponds to the compiler's internal move operation. This would require relaxing restrictions on auto traits and ensuring the compiler move operation still behaved correctly even with the auto-trait.
Because of this complication, a Move trait proposal is likely to first limit itself to only the trivial case. However, this is potentially problematic for future compatibility reasons unless handled very carefully. Consider the following situation with a Move marker trait for the trivial case:
struct Unmovable {
bar: i32
}
impl !Move for Unmovable {}
fn requires_move<T: Move>(x: &T) {}
fn foo() {
let y = 3;
requires_move(y); // Success, y implements Move
let x = Unmovable{ bar: 2 };
requires_move(x); // compile error, x does not implement Move
}
Now, in a later RFC, we want to split Move into an explicit, implicit, and trivial case. The only way to handle this correctly is to change Move from representing a trivial move to representing a fully explicit move, because implicit and trivial both imply an explicit move due to the trait hierarchy. This means that the final set of traits will need to a different name for implicit and trivial move traits, because Move will have to mean an explicit move. Luckily, the change from Move being a marker auto-trait to being a trait with a function with a default implementation isn't a problem, because you don't need to specify the function if it has a default implementation.
However, without modifying how auto-traits work, making something unmovable would require removing three traits, which is both annoying, and makes the migration a breaking change:
struct Unmovable {
bar: i32
}
// Have to do explicit negative impls for all 3 move traits!
impl !Transfer for Unmovable {} // This should ideally just imply the other two.
impl !Relocate for Unmovable {}
impl !Move for Unmovable {}
To facilitate a smooth transition (and to make it less annoying to create unmovable objects), negative trait impls will have to be modified so that, at least for auto-traits, a negative trait impl for Foo would automatically imply a negative trait impl for any auto-trait that has Foo as a supertrait. This would ensure that a negative trait impl for the explicit case would automatically provide negative trait impls for the implicit case and the trivial case. This may naturally arise from simply adding support for auto-traits that have supertraits, but we are explicitly calling it out here regardless.
Logic Should Live in the Explicit Case
Just like how Clone contains the only actual code that controls how copying works, regardless of whether Marker or Copy are implemented, the explicit trait for move must contain the move logic, because the other traits have it as a supertrait. In our previous treatment, the explicit case was Transfer, but as discussed before, the migration pathway for Move means that the explicit case will likely have to be called Move, which must then contain the actual move logic, and two other marker traits for implicit and trivial will need to be created.
Note that one possible implementation of Marker has a separate handle() function, which actually just calls clone(), but allows for making the user's intention more clear:
trait Handle: Clone {
final fn handle(&self) -> Self {
Clone::clone(self)
}
}
This reinforces the idea that the actual non-trivial copy, move, or drop logic should ideally live in the explicit trait. A Move trait proposal should ideally mimic this structure if it provides a seperate implicit non-trivial move trait.
Delete and Destruct Migration Path
Rust currently represents, via an internal compiler trait (that might be exposed in the future), the concept of being destructable via the Destruct trait. Because Rust currently has no concept of an explicit drop, this trait represents the ability for a type to be implicitly dropped. If Destruct is first stabilized only for const contexts, its semantics will need to be expanded so that it can be meaningfully negated outside of const contexts before introducing Delete.
The most obvious way to implement an explicit drop is via a Clone style trait with a delete function, but the existence of Drop means it's not possible to put the custom drop logic in the explicit case without at least some compiler magic (which also creates a non-marker auto-trait):
auto trait Delete {
fn delete(&mut self) {
std::magic::drop(self); // Must call the custom drop code, if it exists for this type.
}
}
Given that implementing explicit dropping would require some compiler magic anyway, because it breaks the assumption that all drops happen as a result of compiler lifetime analysis, we can simply provide a magic function std::mem::delete() that explicitly deletes something. Then, Delete can simply be a marker trait, and the compiler can invoke the drop() logic whenever it deems appropriate.
auto trait Delete {}
auto trait Destruct: Delete {}
auto trait Forget: Destruct {}
trait Drop : Delete {
fn drop(&mut self);
}
This is much easier to implement from the perspective of the compiler, but adding the Delete trait as the explicit case isn't the only option. Another option would be to change Destruct to instead represent explicit drop instead of Delete, and a new trait Destroy can represent the implicit case. This allows more code in the compiler to remain the same - any checks that are trying to see if something can be dropped at all can remain the same, and only places that need to be aware of the difference between an implicit and explicit drop would need to be changed to look for the new Destroy trait.
auto trait Destruct {}
auto trait Destroy: Destruct {}
auto trait Forget: Destroy {}
trait Drop : Delete {
fn drop(&mut self);
}
This shouldn't conflict with the way Destruct is currently being used in const contexts, since it only represents the ability to drop something, not whether it can be implicitly dropped or not. However, if parts of the compiler are not actually using Destruct with these kinds of semantics, it wouldn't actually save much work.
Summary
This RFC is not intended to provide a fully fleshed out implementation of either Move or Delete. Instead, it provides guidance for future RFCs that might implement those traits. This RFC only points out that the following novel concepts will be necessary in the future to support a proper substructural treatment of Move and Delete.
- Allowing auto-traits to have supertraits.
- Expanding
Destroyto apply to non-const contexts. - Modifying the internal drop logic to look for the explicit
Deletetrait, and introducing some method of explicitly dropping an item before it is implicitly dropped. - Adding trait bounds to existing auto-traits to correctly express the substructural hierarchy.
- Any proposed
Movetrait RFC must either restrict itself to the trivial case, or implement the full substructural hierarchy if it wants to allow non-trivial moves. - If no way of adding blanket impls for
TransferandRelocateare found, a future Move RFC tackling non-trivial moves may require support for auto-traits that are not marker traits.
These concepts do not need to be immediately implemented. They should be implemented only when needed to support a future RFC, or after the type solver can handle the extra auto-traits without significant performance losses.
Drawbacks
Attempting to implement this entire RFC all at once would be a considerable undertaking, and is not advised. Instead, this RFC is intended as a guide for more tightly scoped RFCs that each introduce just one or two new traits at a time. It is intended as a roadmap for future changes, and the drawbacks of this RFC can be considered the drawbacks of the child RFCs for each individual trait proposal.
There is a chance that this RFC could be used to bikeshed other proposals and delay their introduction. This is not the purpose of the RFC - what is important is that people are aware of the overarching structure that these traits fit into, which might make it easier to select appropriate names. So far, neither the Forget nor the Handle proposals would need any significant modifications to fit into this RFC. This RFC does put significant constraints on a future Move trait.
All these new autotraits may put some strain on the type checker, but this is already going to happen even with just Forget. The problem is that the compiler is making a lot of assumptions that are not properly formalized, and formalizing them means making them autotraits, because they express properties that the compiler previously assumed applied to all types.
Rationale and alternatives
The rationale here is that Rust is already doing this, just accidentally. The engineering forces are pressuring the Rust ecosystem into slowly and unknowingly building up the table of substructural traits. This RFC simply offers a unifying framework that explains why all these traits are necessary, and to inform current RFC proposals about deep connections between their proposals so they can better coordinate their efforts. Failing to do this will simply mean that the substructural traits will happen anyway - just in a more haphazard and uncoordinated manner.
The greatest risk is currently with the Move trait, especially if a non-trivial implicit move trait is introduced without a corresponding explicit trait. Doing this would effectively make it impossible to ever have an explicit move in the language. A move trait proposal must either implement only a marker trait, or it must jump straight to explicit, implicit, and trivial traits if Rust wants to support non-trivial move logic, otherwise it could trap itself in a type corner.
Potentially avoiding non-marker auto traits
There is an alternative to the move trait hierarchy, by using a Drop trait analogue, ComplexMove, which represents custom move behavior, which is independent from the marker traits that control when a move is permissible.
-- For consistency, Move here still represents the trivial move case, but for practical reasons, it will likely need to represent the explicit case instead.
auto trait Transfer {}
auto trait Relocate: Transfer {}
auto trait Move: Relocate {}
trait ComplexMove : Transfer {
fn move(self: &own Self, rhs: &construct Self);
}
This is arguably less than ideal, but it would avoid the problem of needing auto-traits that are not marker traits. There is no way to avoid handling supertraits on auto-traits, however.
Prior art
Many other languages that implement linear or affine types have some form of substructural type system, or pieces of one. Examples include Idris, Haskell, and Mercury. There is currently no non-garbage-collected systems programming language that intentionally implements a substructural type system. C++ has a different subset, where it implements implicit copies but explicit moves (except in certain cases where implicit moves are allowed):
| Explicit | Implicit | Trivial | |
|---|---|---|---|
| Contraction | N/A | copyable | is_trivially_copyable |
| Weakening | N/A | is_destructible | is_trivially_destructible |
| Exchange | movable | N/A | is_trivially_move_assignable |
Importantly, C++ actually lets you delete the destructor, and will simply refuse to compile any code that would have invoked the destructor. This is a lot easier to do in C++ because new freely allows you to leak memory, and provides prior art for a removable Delete trait.
Unresolved questions
- How to handle auto-traits that have other auto-traits as supertraits? Can we efficiently make
impl !Fooimplyimpl !Bariftrait Bar : Foowithout blowing up the compiler? - How to handle auto-traits that aren't marker traits? Are any modifications necessary to ensure the default implementation of a non-trivial move trait maps to the compiler's trivial move operation?
- If making auto-traits that aren't marker traits is infeasible, what name should be chosen for
ComplexMoveto minimize confusion?
Future possibilities
This RFC offers a pathway to a (nearly) proper substructural type system in Rust, so there are no relevant future possibilities that weren't already discussed.