Skip to content

Instantly share code, notes, and snippets.

@tema3210
Created April 18, 2021 15:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tema3210/fe97c0aa2e2aed953c345794ac88e825 to your computer and use it in GitHub Desktop.
Save tema3210/fe97c0aa2e2aed953c345794ac88e825 to your computer and use it in GitHub Desktop.
An RFC

Summary

Move reference is a new kind of references that are intended to allow moving the value, but not the memory it's stored in.

Motivation

  • Make Box less special by creating mechanism (DerefMove) of moving out of a reference.
  • Solve a concern of temporary moves and panics.
  • Enable placement new to be just a language construct.

Guide-level explanation

&move references provide a way to borrow the memory of a binding while preserving the logic of moving its value.
The type &move T is, in fact, a reference, but unlike other references it allows moving out initialized value of referenced binding.

Core

There are a few types of move references: plain and annotated with ! or *.

About the functionality:

&move T &move T* &move T!
Allows to move out Obligates to move in Both

The use case &move T* reference family is initialization: these references may be used to describe placement features.

&move T! is a move reference to initialized binding with ability to move from it. In fact it can be viewed as mutable reference. The reasons of creating it are simple:

  • It doesn't change existing behavior of &mut T.
  • It is easily implied as a logical continuation of all the syntax of the feature.

Fields of a known type that are unaffected by an operation are simply not mentioned in the header of a move reference type.

Allowing to move out a value implies that it is initialized. So referencing an uninitialized binding by &move T or &move T! is prohibited.

Moving a value into a binding, thus making, or keeping it initialized, is a part of &move T* and &move T! contracts. This is assumed to be true by the compiler, and used in corresponding checks.

Reference-level explanation

Creation

It is allowed to reference a local binding via syntax &move ...

It is also possible to create move reference to a member of a binding referenced by another such reference: as simple as some_reference.some_field - this will produce move reference to the field of referenced binding.

Interaction with patterns:

We introduce a new pattern kind ref move NAME: this produces NAME of type &move T!. The reason of the ! obligation is that we may not want to left a binding (partially) deinitialized after execution of a pattern-matching construct.

Subtyping:

move references have the following subtyping rules:

  • &move T! is a subtype of &move T* for all T.
  • &move T(*list of covered fields*) is a subtype of &move T(*another list*) for all T if and only if first list mentions exactly the same fields as does second, and every mentioned field of the first guarantees the same as corresponding field of the second.

DerefMove trait

I also propose design of DerefMove:

trait DerefMove {
  type Output;

  fn deref(&move self!) -> &move Self::Output!;
}

The reason of such design is that we may not want allow pattern matching to partially deinitialize a binding, as it will require way more complex analysis as well as will give pattern matching powers that do not align well with its original purpose (expressing "high level" logic).

The Box implementation:

struct Box<T>{
  ptr: *mut T,
}

...

impl<T> DerefMove for Box<T> {
  type Output = T;

  fn deref(&move self!) -> &move T! {
    self.ptr as &move T! //just cast the pointer to a reference
  }
}

Aliasing:

Given that all move references are intended to modify referenced binding they all must be unique as &mut T is.

Interaction with panics:

The representation of a move reference may include not only the pointer itself, but also a bitfield storing information of whether anything was moved out of reference or not.
This allows to get rid of concerns about drops of uninitialized data during panics.

I guess, this may look like:

#[repr(C)]
struct MoveRef<T> {
  ptr: *mut T, //pointer.
  flags: MoveFlagsOf<T>, //of course, this is not real type, but a kind of intrinsic rather.
}

Also, due to reference being unique there is no need in bitfield being atomic.

Optimization

Another key property of move references, is that their usage implies moving the value in and out: this is the perfect case for GCE.

We may want to do GCE for &move references.
In this case move flags should live on stack.

!Unpin types

Due to the fact that moving a value of !Unpin type most likely will corrupt the data, we may not want it to be moved out from such a reference.

Pin, DerefMove and stack pinning

The impls are following:

impl<P,T> DerefMove for Pin<P>
  where P: DerefMove,
{
  type Output = P::Output;

  fn deref(&move self!) -> &move Self::Output! {
    self.pointer
  }
}

impl<P> Pin<P>
  where P: DerefMove
{
  pub fn new_move(ptr: P) -> Pin<P> {
    Pin {pointer: ptr}
  }
}

An example of use:

...
fn main() {
  let g = make_some_not_unpin_gen();
  let pinned = Pin::new_move(&move g!);
  //work with it!
}
...

Drawbacks

  • This adds an entire kind of references. We'll need to teach this.

Rationale and alternatives

The feature serves one need: moving a value but not the memory.

Alternatives are either on the library level or in previous proposals.

Prior art

Unresolved questions

  • Should we allow coercing &mut T to &move T! and vice versa?
  • Is the way the DerefMove trait is defined here right? What's about another kinds of move references?
  • Are the annotations done right? We may want to exchange semantics of default and some of annotated syntaxes.

Future possibilities

Partial initialization

Partial initialization of a binding of a known type C can be described via following syntax: &move C(a!,b*,c,...).

An example:

struct C {
  a: String,
  b: String,
  c: String,
  d: u32,
}

/// ...Promises to init `b`, keep `a` and uninit `c`, doesn't touch `d` at all.
fn work(arg: &move C(a!,b*,c,.d)) { //dot prefixed `d` may have been omitted.
  let mut tmp = arg.a; //we moved the String to `tmp`
  tmp.append(&arg.c) //we may not move the 'arg.c', but we haven't gave a promise to initialize it back.

  arg.a = tmp; //we initialized `arg.a` back; removing this line is hard error.

  arg.b = "init from another function!".into();

  //println!("{:?}",arg.d ); //error: use of possibly uninitialized value.
}

fn main() {
  let trg: C;
  trg.a = "Hello ".into();
  trg.c = " Hola".into();

  work(&move trg);
  println!(&trg.b); //legal, as work gave a promise to initialize
  println!(&trg.a); //legal
  //println!(&trg.c); //error: use of definitely uninitialized value.

}

Tuples

Syntax of &move references with partial initialization of a tuples is following:

Given a tuple (u32,i64,String,&str) the move reference syntax is like: &move (.u32,i64,String!,&str*) - note the dot prefixed u32 - it will not be touched by a consumer of a reference, but is here to distinguish different tuple types from one another (in cases of named structures untouched fields are simply not mentioned).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment