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]whereTis 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_useattributes)
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:
@copytypes are duplicated implicitly when used@handletypes require explicit.handle()calls for duplication@copyis appropriate for small, cheap-to-copy types (likePoint)@handleis 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
}