Idea: Alias as a common abstraction over Share and Reborrow

I have been thinking about the relationship between the proposed Reborrow work and the recent Share/cheap-clone ideas.

I think there may be a useful common abstraction above them: (Updated full design here)

unsafe auto trait Alias {}

trait Share { .. }
trait Reborrow { .. }

The basic idea is that:

Alias means another access path to the same underlying resource can be created without moving or Copying the original, by either sharing or reborrowing it.

Share and Reborrow are then two different aliasing disciplines.

For a shared alias:

Arc<T>: Share

creating another alias produces another independently owned handle to the same underlying resource. Both handles remain fully usable. Share is intended to describe shared ownership, not shared reference access.

For a reborrowed alias:

&mut T: Reborrow

creating another alias produces a temporary borrowed view of the same underlying resource. The original remains owned by the caller, but its access is restricted for the lifetime of the reborrow, according to the usual borrowing rules.

So conceptually:

             Alias
            /     \
        Share     Reborrow

Consider:

struct X<'a, T, C> {
    a: Option<&'a mut T>, // Reborrow -> Alias
    b: Arc<C>,            // Share    -> Alias
}

Here, a participates in reborrowing, while b participates in sharing.

A temporary alias of X is not something you manually construct by rewriting fields. Instead, it is simply a derived view of X that the compiler can form when needed, where:

  • a is reborrowed automatically (if accessed mutably)
  • b is shared (cheaply cloned or reference-counted)

So conceptually, the compiler treats aliasing X as producing a new view:

X (aliased view)
{
    a: reborrowed view of x.a,
    b: shared view of x.b,
}

Copy fields and metadata

I do not think ordinary Copy fields should automatically imply Alias.

Consider:

struct X<'a, T> {
    a: &'a mut T,
    has_been_modified: bool,
}

If aliasing X simply copies the bool, then two aliases could diverge:

  • one alias mutates a and sets has_been_modified = true
  • another alias still sees false

Now the two handles disagree about state that is meant to describe the same logical resource.

The compiler cannot know whether a Copy field is:

  • harmless metadata (len, capacity, etc.)
  • or logically significant state tied to the resource

So the safe default must be conservative:

struct automatically implements Alias
iff every field implements Alias

Copy alone is not sufficient evidence of alias-safety.


Safe opt-in for metadata-heavy types

Types that intentionally include duplicated metadata can opt in manually:

struct SliceMut<'a, T> {
    a: &'a mut T, // Reborrow, therefore Alias
    len: usize,   // Copy, therefore !Alias
}

unsafe impl<T> Alias for SliceMut<'_, T> {}

Here, len is just descriptive metadata, so copying it as part of an alias is fine. The unsafe impl is the author's promise that doing so cannot break the type's invariants.

But not every handle-like type should be able to opt in this way.

For example, MutexGuard<'_, T> carries the responsibility of unlocking the mutex when it is dropped. Creating another guard would duplicate that responsibility, so it should not be Alias.

Roughly:

SliceMut<'_, T>   -> explicit Alias is reasonable
Arc<Mutex<T>>     -> Share -> Alias
&mut T            -> Reborrow -> Alias
MutexGuard<'_, T> -> !Alias

So Alias is not simply "this type refers to something".


How this would look in practice

Shared case (Share)

fn use_shared<T: Share>(x: T) {
    foo(x);  // x.share()
    boo(x);  // x.share()
}

Both aliases are independent and fully usable.


Reborrow case (Reborrow with auto-reborrowing)

fn use_reborrow<'a, T>(x: Option<&'a mut T>) {
    foo(x); // compiler may implicitly reborrow
    boo(x); // reborrow rules ensure safety
}

Here, the user does not manually construct aliases. The compiler inserts reborrows as needed, and ensures that all derived uses respect exclusivity.


Mixed struct case (Alias composition)

struct X<'a, T, C> {
    a: Option<&'a mut T>,
    b: Arc<C>,
}
fn process<'a, T, C>(x: X<'a, T, C>) {
    foo(x);
    boo(x);
}

Conceptually:

  • a is reborrowed automatically when needed
  • b is shared via Arc

The important point is that the whole struct can be treated as an Alias, even though its fields use different aliasing disciplines.


Mutex example

fn use_mutex<T>(m: Arc<Mutex<T>>) {
    foo(m); // Share
    boo(m); // Share
}

Here Arc<Mutex<T>> is Share, hence Alias.

But:

fn use_guard<T>(g: MutexGuard<'_, T>) {
    foo(g);
    boo(g); // invalid: would duplicate unlock responsibility
}

MutexGuard is not Alias, because aliasing it would duplicate ownership of the unlock action.

However, the inner &mut T remains a valid Reborrow.


Closing thought

I am curious whether this has already been explored as an explicit trait relationship.

In particular, could Alias be useful as a compiler-known auto trait that:

  • composes structurally across Share and Reborrow
  • is automatically derived when all fields are Alias
  • requires explicit unsafe opt-in when a type includes duplicated metadata that is not itself Alias

It feels like this might be a unifying abstraction over several existing and proposed ownership mechanisms.

I forgot to emphasize one point: this feels like what Alias should mean in Rust, the safe ability to create another alias to the same resource, not necessarily the cheap(est) ability to do so.

New design below.

Use
β”œβ”€β”€ Move
β”‚     transfer the value
β”‚
β”œβ”€β”€ Copy
β”‚     duplicate the value
β”‚
β”œβ”€β”€ Alias
β”‚   β”œβ”€β”€ Share
β”‚   β”‚     create another independently usable handle
β”‚   β”‚
β”‚   └── Reborrow
β”‚         create a temporary handle;
β”‚         restrict the source while it exists
β”‚
└── Clone (not really important here because it's explicit)
      create another value

Maybe this is closer to how it needs to look:

#[auto_stop(
    Drop,
    Copy,
    size_of::<Self>() == 0,
    !Freeze
)]
#[lang="alias"]
unsafe auto trait Alias {}

trait Share: Clone { .. }

#[lang="reborrow"]
trait Reborrow { .. }

impl<'a, T: ?Sized> Reborrow for &'a mut T { .. }
unsafe impl<'a, T: ?Sized> Alias for &'a T {}

unsafe impl<T: Share /* + !Reborrow */> Alias for T {}
unsafe impl<T: Reborrow /* + !Share */> Alias for T {}

The idea is:

Copy     -> stops auto Alias
Clone    -> says nothing about Alias
Share    -> Alias
Reborrow -> Alias

----------------------------------

T: Copy + Share             -> not allowed
T: Copy + Reborrow          -> not allowed
T: Copy + Share + Reborrow. -> not allowed
T: Share + Reborrow         -> most likely also not allowed

For example:

struct X<'a, T> {
    data: &'a mut T, // Reborrow -> Alias
    name: String,    // Clone only
}

String is not Alias, so X would not automatically become Alias.

Likewise:

struct X<'a, T> {
    data: &'a mut T,
    description: Cow<'a, str>,
}

Cow is Clone, but cloning it can either preserve a borrow or create independent owned data. So Clone clearly cannot be enough to establish Alias.

A custom mutable reference is the opposite example:

struct MyMut<'a, T> {
    ptr: NonNull<T>,
    marker: PhantomData<&'a mut T>,
}

Its fields are mostly Copy, so it would not become Alias structurally. Instead, implementing Reborrow explicitly gives it the intended aliasing semantics. (Depends on the definition)

This feels like a useful boundary: Alias should come from actual Share/Reborrow semantics, not merely from being Copy or Clone.

How an Alias is formed

Once a type is known to be Alias, the compiler could construct the aliased value field-by-field.

For each field, it picks the strongest applicable operation:

Reborrow
Share
Clone

For example:

struct X<A, B, C> {
    a: A, // Reborrow + Clone -> Reborrow
    b: B, // Share + Clone    -> Share
    c: C, // Copy + Clone     -> Clone
}

So Clone is only a fallback for fields that do not themselves have aliasing semantics.

This fallback would only be available because the containing type has been explicitly certified with:

unsafe impl Alias for X<...> {}

If a field supports none of Reborrow, Share, or Clone, I think the simplest option is to reject it. Alternatively, unsafe impl Alias could explicitly promise that such a field may be bitwise reproduced, but that is a much stronger contract and needs more thought.

Concern: when should implicit aliasing happen?

One part I am still unsure about is exactly when the compiler should choose Alias instead of Move.

For example:

async fn test(x: Arc<C>) -> Arc<C> {
    tokio::spawn(async move { foo(x) }); // alias?
    x                                   // move?
}

A liveness-based model could make the async move capture implicitly share x, because the original is used again afterwards:

let captured = x.share();

tokio::spawn(async move {
    foo(captured);
});

x

The same question appears in loops:

loop {
    x.something(); // implicit Share every iteration?
}

This seems useful, but Share is not necessarily free. For something like Arc, this could mean a refcount increment/decrement on every iteration.

Maybe some cases should therefore produce a lint:

warning: `x` is implicitly shared on every loop iteration
help: use `x.share()` to make this explicit

Likewise, an implicit share into an async move block or spawned task may keep the underlying value alive for much longer than expected, so that may also be worth linting.

I think Reborrow is different here: implicit reborrowing is already normal Rust behavior and generally should not need such a warning.

So perhaps the distinction is:

Reborrow -> implicit by default

Share    -> may be implicit when required by liveness,
            but lint when it is repeated, escaping,
            or otherwise potentially surprising

I am not yet sure whether liveness alone is the right rule, but Alias should only mean that the transformation is safe. Whether an implicit Share is cheap or desirable can be handled separately by lints.

Use of x:
  move(x)
  copy(x)
  alias(x)

NOTE: The only concern might be if you have 100 fields in an ADT that are Alias, that could be tricky. Maybe a lint for that too?

Could you please explain how Alias can be used? I mean, what can we do with T: Alias, or how it may affect the compilation?

1 Like

I have been a bit vague about those parts, especially around the why. The intent is that T: Alias lets a by-value use avoid consuming x when x is needed again.

Conceptually:

move(x)
copy(x)
alias(x) // use(x) could also work? IDK

Alias says alias(x) is sound; Share/Reborrow define how it happens field-wise.

This idea is a bit inspired by Swift lang, more specifically, ARC. The system manages sharing behind the scenes, keeping the code more implicit and ergonomic.

This idea is also in the same spirit as making Rust more ergonomic: reducing unnecessary moves and making shared access feel more natural without losing safety.

UPDATE: Just know that I am not the one who thought of this ergonomic goal. I've read Niko's blog posts and picked up some of these ideas from him.

Do you mean this? Share is an implicit .clone() inserted due to low cost of doing a shallow-copy, and Reborrow is a shorter-lifetimed temporary created implicitly, shadowing the lender temporarily and returning back to the lender at the end of its lifetime. So you pack up these 2 implicit behaviors together and say it Alias, right?

Yeah, kind of.

I'm basing the Share trait on this goal (but making &T not Share), and the Reborrow trait on this goal.

               Alias 
              /     \
     [persistent]  [temporary]
          |           |       \
        Share      Reborrow    &T
                      |
                 CoerceShared

To be a little more precise.

Alias is the auto trait that tries to unify these behaviors, letting you implicitly alias things in a safe way. Then the language can remain implicit by default, while lints guide the programmer toward making intent explicit in places where that matters (this part is non-trivial).

The language doesn't even have to be implicit by default. It can be that when creating a new rust project, you can choose "brainstorming mode", which would "disable" the explicitness (lints), letting you code as if you were in a high-level language.