Move Semantics

This section describes how values are moved and copied in Rue.

Value Categories

Types in Rue are categorized by how they behave when used:

  • Copy types can be implicitly duplicated when used. Using a Copy type does not consume the original value.
  • Move types (also called affine types) are consumed when used. After using a move type value, the original binding becomes invalid.

The following types are Copy types:

  • All integer types (i8, i16, i32, i64, u8, u16, u32, u64)
  • The boolean type (bool)
  • The unit type (())
  • Enum types (all variants of an enum)
  • Array types [T; N] where T is a Copy type

User-defined struct types are move types by default. Using a struct value consumes it.

struct Point { x: i32, y: i32 }

fn main() -> i32 {
    let p = Point { x: 1, y: 2 };
    let q = p;      // p is moved to q
    // p is no longer valid here
    q.x + q.y
}

The @copy Directive

A struct type MAY be declared as a Copy type using the @copy directive before the struct definition.

copy_struct = "@copy" struct_def ;

A struct marked with @copy is a Copy type. Using a @copy struct value does not consume it; the value is implicitly duplicated.

@copy
struct Point { x: i32, y: i32 }

fn main() -> i32 {
    let p = Point { x: 1, y: 2 };
    let q = p;      // p is copied, not moved
    let r = p;      // p can be used again
    p.x + q.x + r.x // all three are valid
}

A @copy struct MUST contain only fields that are themselves Copy types. It is a compile-time error to mark a struct as @copy if any of its fields are move types.

struct Inner { value: i32 }  // move type (no @copy)

@copy
struct Outer { inner: Inner }  // ERROR: field 'inner' has non-Copy type 'Inner'

A @copy struct MAY contain fields of primitive Copy types (integers, booleans, unit), enum types, arrays of Copy types, or other @copy struct types.

@copy
struct Point { x: i32, y: i32 }

@copy
struct Rect { top_left: Point, bottom_right: Point }  // OK: Point is @copy

fn main() -> i32 {
    let r = Rect {
        top_left: Point { x: 0, y: 0 },
        bottom_right: Point { x: 10, y: 10 }
    };
    let r2 = r;     // r is copied
    r.top_left.x    // r is still valid
}

Linear Types

A struct type MAY be declared as a linear type using the linear keyword before the struct definition.

linear_struct = "linear" "struct" IDENT "{" [ struct_fields ] "}" ;

A linear type MUST be explicitly consumed. It is a compile-time error for a linear value to go out of scope without being consumed by a function call.

A linear value is consumed when it is:

  • Passed as an argument to a function (the function is the consumer)
  • Returned from a function (the caller becomes responsible for consuming it)
  • Field access is performed on the value (the value is destructured)
linear struct MustUse { value: i32 }

fn consume(m: MustUse) -> i32 { m.value }

fn main() -> i32 {
    let m = MustUse { value: 42 };
    consume(m)  // OK: m is consumed
}

It is a compile-time error to allow a linear value to be implicitly dropped.

linear struct MustUse { value: i32 }

fn main() -> i32 {
    let m = MustUse { value: 1 };  // ERROR: linear value dropped without being consumed
    0
}

A linear struct MUST NOT be marked with @copy. Linear types cannot be implicitly copied.

@copy
linear struct Invalid { value: i32 }  // ERROR: linear types cannot be @copy

Linear types are useful for:

  • Resources that must be explicitly released (file handles, database transactions)
  • Protocol enforcement (ensuring state machine transitions are completed)
  • Results that must be checked (similar to must_use attributes)

The @handle Directive

A struct type MAY be declared as a handle type using the @handle directive before the struct definition. Handle types support explicit duplication via a .handle() method.

handle_struct = "@handle" struct_def ;

A struct marked with @handle MUST provide a method named handle with the following signature:

fn handle(self) -> T

where T is the handle struct type. It is a compile-time error to mark a struct with @handle if it does not provide this method.

The handle method MUST take exactly one parameter (self of the struct type) and MUST return the same struct type. It is a compile-time error if the method signature differs.

@handle
struct Counter { count: i32 }

impl Counter {
    fn handle(self) -> Counter {
        Counter { count: self.count }
    }
}

fn main() -> i32 {
    let a = Counter { count: 1 };
    let b = a.handle();  // explicit duplication
    b.count
}

Calling .handle() on a handle type does not consume the receiver and returns a new owned value. Both the original and the returned value are valid after the call.

Handle types are useful for:

  • Reference-counted types (Rc, Arc) where duplication increments the count
  • Interned strings where duplication is cheap
  • Shared resources where explicit duplication makes cost visible

A @copy struct implicitly supports handle semantics. Any @copy type can be explicitly duplicated, although the .handle() method is not required.

The difference between @copy and @handle:

  • @copy types are duplicated implicitly when used
  • @handle types require explicit .handle() calls for duplication
  • @copy is appropriate for small, cheap-to-copy types (like Point)
  • @handle is appropriate for types where duplication has visible cost (like reference-counted types)

A linear struct MAY be marked with @handle if explicit duplication is meaningful (e.g., forking a transaction).

Use After Move

It is a compile-time error to use a value that has been moved.

struct Point { x: i32, y: i32 }

fn main() -> i32 {
    let p = Point { x: 1, y: 2 };
    let q = p;      // p is moved
    let r = p;      // ERROR: use of moved value 'p'
    0
}

A value is considered moved when it is:

  • Assigned to another variable
  • Passed as an argument to a function
  • Returned from a function
struct Data { value: i32 }

fn consume(d: Data) -> i32 { d.value }

fn main() -> i32 {
    let d = Data { value: 42 };
    let result = consume(d);  // d is moved into the function
    // d is no longer valid here
    result
}

Copy Types and Multiple Uses

Copy types can be used multiple times without being consumed.

fn main() -> i32 {
    let x = 42;
    let a = x;  // x is copied
    let b = x;  // x is copied again
    a + b       // 84
}

Function parameters of Copy types receive a copy of the argument. Function parameters of move types receive ownership of the argument.

Partial Moves (Field-Level Moves)

When a non-Copy field of a struct is accessed (moved out of), only that specific field is moved, not the entire struct. Other fields remain accessible.

struct Inner { x: i32 }
struct S { a: Inner, b: Inner }

fn main() -> i32 {
    let s = S { a: Inner { x: 1 }, b: Inner { x: 2 } };
    let x = s.a;   // Only s.a is moved
    let y = s.b;   // s.b is still valid
    x.x + y.x      // 3
}

It is a compile-time error to access a field that has already been moved.

struct Inner { x: i32 }
struct S { a: Inner, b: Inner }

fn main() -> i32 {
    let s = S { a: Inner { x: 1 }, b: Inner { x: 2 } };
    let x = s.a;   // s.a is moved
    let z = s.a;   // ERROR: use of moved value 's.a'
    0
}

A struct with any moved fields cannot be used as a whole value. It is a compile-time error to move or pass the struct after any of its non-Copy fields have been moved.

struct Inner { x: i32 }
struct S { a: Inner, b: Inner }

fn consume(s: S) -> i32 { s.a.x + s.b.x }

fn main() -> i32 {
    let s = S { a: Inner { x: 1 }, b: Inner { x: 2 } };
    let x = s.a;   // s.a is moved (partial move)
    consume(s)     // ERROR: use of moved value 's' (partially moved)
}

Accessing Copy-type fields does not move them. Copy-type fields can be accessed any number of times without affecting the struct's move state.

struct S { a: i32, b: i32 }

fn main() -> i32 {
    let s = S { a: 1, b: 2 };
    let x = s.a;   // s.a is copied
    let y = s.a;   // s.a can be copied again
    let z = s.b;   // s.b is also valid
    x + y + z      // 4
}

Shadowing and Moves

Shadowing a variable does not prevent it from being moved. A moved variable remains invalid even if a new variable with the same name is introduced in an inner scope.

struct Data { value: i32 }

fn main() -> i32 {
    let d = Data { value: 1 };
    let x = d;  // d is moved
    {
        let d = Data { value: 2 };  // New 'd' shadows, but doesn't restore old 'd'
        d.value
    }
    // Original 'd' is still invalid here
}