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 (3.8:76):
- Copy types are implicitly duplicated by a use; using a Copy value does not consume the original.
- Move types (also called affine types) are consumed by a use; after a move type value is used, 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
}
Using a Value
The single operation underlying moves, copies, and linear consumption is the use of a value. Every occurrence of a place expression — a binding, a field projection, or an array index — sits in exactly one of two syntactic contexts. In a place context the occurrence denotes a location and the value stored there is not consumed by appearing there; the place contexts are exactly the target of an assignment, the base of a field projection or array index, and the operand of a borrow or inout argument. Every other occurrence is a value context — an operator operand, the scrutinee of an if or match, a struct-field or array-element initializer, a by-value function argument, the operand of return, or the tail expression of a block — and such an occurrence is a use of the place. The effect of a use depends only on the value category of the place's type: a use of a Copy type copies the value and leaves the place valid (3.8:9); a use of a move type, whether affine or linear, moves — equivalently, consumes — the value, leaving the place invalid until it is reinitialized (3.8:7, 3.8:33). A use of a field projection or a constant array index moves only that sub-place, a partial move (3.8:22). The enumerations below — the move contexts (3.8:7), the linear consumption contexts (3.8:33), and the repeated use of Copy values (3.8:9) — are each a consequence of this one definition: which occurrences are value contexts, and which value category the used type has. The core calculus, docs/formal/01-core-calculus.md §4.2, states this precisely (place context versus value context, and the copy-versus-move effect of a use); the present paragraph is its informal gloss, and the formal definition governs.
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.
Consuming a linear value is the same operation as moving it: any use of a linear value (3.8:76) consumes it. Passing it as a by-value argument (the function becomes the consumer), returning it (the caller becomes responsible for consuming it), and moving a field out of it (a partial move that destructures it, 3.8:22) are all uses, and therefore each consumes the value.
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
A linear value MUST be consumed on every control-flow path on which it goes out of scope. It is a compile-time error for a linear value to be consumed in only some branches of a conditional (if/else or match): the paths that do not consume it would drop it implicitly.
A control-flow path that diverges (for example, by executing a return expression) does not reach the end of the value's scope, and so is exempt from the consumption requirement on that path.
linear struct MustUse { value: i32 }
fn consume(m: MustUse) -> i32 { m.value }
fn main() -> i32 {
let m = MustUse { value: 1 };
if true {
consume(m) // ERROR: 'm' is not consumed on the else path
} else {
0
}
}
A type carries a linear value if it is a linear struct type, an array type whose element type carries a linear value, or a struct type with a field whose type carries a linear value. Pointer types do not carry a linear value (a pointer does not own its pointee).
Linearity is infectious: a struct type that is not declared linear but has a field whose type carries a linear value is itself a linear type. If the containing struct could be implicitly dropped, the linear field would be silently dropped with it.
linear struct MustUse { value: i32 }
struct Wrap { m: MustUse } // not declared linear, but linear by 3.8:58
fn main() -> i32 {
let w = Wrap { m: MustUse { value: 1 } }; // ERROR: linear value 'w' dropped
0
}
It is a compile-time error for a field access that consumes a linear value (destructuring, 3.8:33) to implicitly drop a different field that carries a linear value. Every struct level along the access path is checked: at each level, all fields other than the accessed one are dropped by the destructure.
linear struct MustUse { value: i32 }
struct Container { inner: MustUse, tag: i32 }
fn sink(m: MustUse) -> i32 { m.value }
fn main() -> i32 {
let c = Container { inner: MustUse { value: 1 }, tag: 2 };
c.tag // ERROR: would implicitly drop linear field 'inner'
}
fn ok() -> i32 {
let c = Container { inner: MustUse { value: 1 }, tag: 2 };
sink(c.inner) // OK: extracts the linear field; 'tag' (non-linear) is dropped
}
A function owns its pass-by-value parameters and drops them when it returns unless they are moved out. Therefore a pass-by-value parameter whose type carries a linear value MUST be consumed by the function body on every non-diverging control-flow path, exactly as for a linear local binding (3.8:32, 3.8:50). borrow and inout parameters are exempt: the caller retains ownership. A destructor's self parameter is also exempt: it is disposed of by the drop glue after the destructor body runs (see 3.9), and moving it out is rejected.
linear struct MustUse { value: i32 }
fn bad(m: MustUse) -> i32 { 0 } // ERROR: 'm' is dropped, not consumed
fn good(m: MustUse) -> i32 { m.value } // OK: destructuring consumes 'm'
It is a compile-time error to discard an expression value whose type carries a linear value. A value is discarded when it is the value of a non-final expression statement in a block, or the result value of a loop body (which is discarded on every iteration).
linear struct MustUse { value: i32 }
fn make_linear() -> MustUse { MustUse { value: 1 } }
fn main() -> i32 {
make_linear(); // ERROR: discarded linear value
0
}
The consumption requirement (3.8:32) applies to every binding whose type carries a linear value (3.8:57), not only to bindings of linear struct type. In particular, an array whose element type carries a linear value MUST be consumed — either as a whole (for example, by passing the array to a function by value) or element-wise via constant-index moves (3.8:71); dropping the array would silently drop every element.
linear struct MustUse { value: i32 }
fn make_linear() -> MustUse { MustUse { value: 1 } }
fn main() -> i32 {
let a = [make_linear(), make_linear()]; // ERROR: 'a' is dropped, not consumed
0
}
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)
Use After Move
It is a compile-time error to use (3.8:76) 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 move type value is moved by any use of it (3.8:76). Assigning it to another binding, passing it as a by-value argument, and returning it from a function are all value-context occurrences, and are therefore all moves.
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
A use of a Copy type (3.8:76) copies the value and leaves the original valid, so a Copy value may be used any number of 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
}
Passing an argument by value is a use of it (3.8:76): a parameter of Copy type receives a copy of the argument, and a parameter of move type receives ownership by moving the argument.
Partial Moves (Field-Level Moves)
A use of a non-Copy field projection (3.8:76) is a partial move: only that specific field is moved, not the entire struct, and the sibling 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)
}
A use of a Copy-type field (3.8:76) copies it and does not move it; Copy-type fields can therefore 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
}
The base of a field projection is read in place context (3.8:76), but it must still own its storage. Accessing a field through a moved ancestor path is therefore a compile-time error even when the accessed field is itself a Copy type: the moved ancestor's storage is no longer owned by the variable, so nothing within it may be read.
struct Inner { x: i32 }
struct Outer { f: Inner }
fn consume(i: Inner) -> i32 { i.x }
fn main() -> i32 {
let o = Outer { f: Inner { x: 1 } };
let a = consume(o.f); // o.f is moved
let b = o.f.x; // ERROR: use of moved value 'o.f.x'
a + b
}
Assigning a new value to a moved field reinitializes it. After the assignment, the field (and any of its subfields) may be used again.
struct Inner { x: i32 }
struct Outer { f: Inner }
fn consume(i: Inner) -> i32 { i.x }
fn main() -> i32 {
let mut o = Outer { f: Inner { x: 1 } };
let a = consume(o.f); // o.f is moved
o.f = Inner { x: 2 }; // o.f is reinitialized
let b = o.f.x; // OK: o.f is valid again
a + b // 3
}
Array Element Moves
Indexing an array variable with a compile-time constant index whose element type is not Copy moves that element out of the array. Only that element is invalidated; sibling elements remain usable and are still dropped normally. Element moves are tracked only for indexing applied directly to an array variable (or by-value array parameter); an array reached through another projection, or indexed with a non-constant index, cannot be moved out of (see the legality rule in the Arrays chapter).
struct Big { value: i32 }
fn consume(b: Big) -> i32 { b.value }
fn main() -> i32 {
let xs = [Big { value: 1 }, Big { value: 2 }];
let a = consume(xs[0]); // moves only xs[0]
let b = consume(xs[1]); // xs[1] is still valid
a + b // 3
}
While one or more elements of an array are moved out, it is a compile-time error to use the moved element (including reading a field through it), to use the array as a whole value, or to index the array with a non-constant index. The non-constant-index restriction is required for soundness: the compiler cannot know at compile time whether a runtime index denotes a moved-out element.
An array whose elements carry linear values may be consumed element-wise: its must-consume obligation is satisfied when every element has been moved out on every non-diverging path. Moving out only some elements, or moving an element on only some paths, is a compile-time error naming the elements that are not consumed.
While one or more elements of an array are moved out, it is a compile-time error to assign into the array — to an element, or through an element (e.g. to a field of an element). Element writes do not reinstate per-element ownership; the whole array must be reinitialized instead, which makes every element owned (and droppable) again.
At scope exit (and when an array variable is overwritten), elements that were moved out on every path reaching that point are not dropped; elements moved out on only some paths are dropped exactly when the executed path did not move them; untouched elements are dropped, in ascending index order.
A zero-length array of a linear element type holds no linear values, so its must-consume obligation is vacuously satisfied: it may be dropped (as a local, a by-value parameter, or a discarded expression value) without error. This applies to any array shape whose total element count is zero (for example [L; 0], [[L; 5]; 0], and [[L; 0]; 5]). It does not apply to a linear struct itself: a value of a linear struct type must be consumed regardless of what its fields hold.
linear struct MustUse { value: i32 }
fn main() -> i32 {
let _none: [MustUse; 0] = []; // OK: nothing to consume
0
}
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
}