Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025

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 floating-point types (f32, f64) (3.12:2a)
  • The unit type (())
  • The first-class string types: str (3.7:44) and the fixed inline buffers Str(N) (3.7:50)
  • Discriminant-only enum types (every variant is payload-free, C-like)
  • Array types [T; N] where T is a Copy type

A payload-carrying enum is not unconditionally Copy: its multiplicity is the join of its variants' payload multiplicities over Copy ⊑ Affine ⊑ Linear (6.3:19). Such an enum is Copy only when every payload is itself Copy; a variant carrying a move (affine or linear) payload makes the whole enum a move 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, the operand of a borrow or inout argument, and the operand of an equality comparison (==/!=), which is read through a shared borrow rather than consumed (4.3:3f). Every other occurrence is a value context — an arithmetic, bitwise, or ordering 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), first-class string types (str, Str(N) — 3.7:44, 3.7:50), discriminant-only enum types, arrays of Copy types, or other @copy struct types. A field whose type is a payload-carrying enum is admissible only when that enum is itself Copy under the join of 6.3:19 — that is, only when every one of its payloads is Copy; a @copy struct field must itself be a Copy type (3.8:18), so a move-typed enum field is rejected.

@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) and returning it (the caller becomes responsible for consuming it) are both uses, and therefore each consumes the value. Moving a field out of a value whose struct type is declared linear destructures it: because the obligation belongs to the value itself and not to its contents (3.8:74), the field access consumes the smallest enclosing declared-linear place and produces the selected field. All droppable residue in that place is destroyed immediately, exactly once, in declaration/ascending-index order. A residue that carries a linear obligation is not silently dropped: the projection is a compile-time error (3.8:60), and a whole-value consumption or complete consuming destructure is required. The value so consumed is the place that is destructured, which is the binding as a whole only when the declared-linear struct is the binding's own type: h.arr[0].v destructures the element h.arr[0], and h.a.v the field h.a. Every sub-place of an enclosing value that is not part of the destructured value — a sibling element, a sibling field, the rest of the binding — is untouched by the destructure: it keeps its own must-consume obligation (3.8:32) and is dropped by the ordinary scope-exit walk (3.9:2). When the access chain passes through several declared-linear levels, the smallest (innermost) one is destructured; declared-linear ancestors retain their own obligations and enclosing residue. A field access on a struct that is linear only by infection (3.8:58) is not a destructure; it is the ordinary partial move of 3.8:22, and the carrier's obligation is discharged sub-place by sub-place (3.8:60).

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 before a scope's end never reaches that scope's end, but this reachability fact is not a general exemption from consumption. An explicit, statically known @panic is the sole aborting-edge exemption: it abandons the process without unwinding language scopes, runs no drop glue, and therefore requires no linear consumption for bindings live on that edge. Ordinary ! calls, infinite loops, and other generic divergence are not panic edges and remain subject to the conservative must-consume check at the edge. An exit that unwinds scopes — a return expression (including the implicit return of a ? expression's failure path, 4.15:7), a break, or a continue — ends the scopes it exits at the exit itself and drops their live bindings there (4.9:7, 4.8:21), so a linear value held by any scope the exit unwinds — including, for a return or ?, a pass-by-value parameter (3.8:62) — MUST already have been consumed in the state in force when the exit executes. It is a compile-time error otherwise, exactly as at the scope's end (3.8:32, 3.8:50): the exit's drops would otherwise destroy the value unconsumed. If an expression has both a panic edge and a non-panic edge, the non-panic edge remains subject to this check.

For a loop, the ownership state of every binding whose root is outside the loop's scope is checked at the loop's head state: the ownership state on entry to the loop joined, per binding, with the state on every reachable back edge into the loop head, using the same join as for conditionals (3.8:50) and repeated until the head state no longer changes. The loop body MUST be well-formed when checked from the head state. So a move of a binding in one iteration is a compile-time error if a later iteration can reach a use of that binding before reinitializing it, while a body that reinitializes the binding before every use on every path (for example, loop { d = mk(); consume(d); }) is well-formed. A linear-carrying binding MUST have the same ownership state on entry and on every reachable back edge, because the join of a consumed and an unconsumed linear value is not defined (3.8:50). A back edge is reachable when control flow can return to the next iteration through ordinary body completion that reaches that iteration or a continue targeting this loop; a path that diverges or exits with break does not contribute a back-edge state. Bindings rooted inside the loop are loop-local and are not part of the head state: their scopes end within the iteration. If no back edge is reachable — for example, when every reachable path breaks — the head state is the entry state. An unreachable ordinary completion or targeting continue edge likewise contributes no ownership state.

The ownership state after a loop MUST be the join of the ownership states on all reachable edges that exit that loop, with each exit's state taken from the body checked at the head state (3.8:79), so an exit taken on a later iteration carries the moves of earlier iterations. For a while, these are the reachable false-condition exits at the initial zero-iteration check and at every later condition check, together with reachable break edges targeting that loop. For a for, they are reachable iterator-exhaustion exits together with such break edges. For a loop, they are only reachable break edges targeting that loop. A return or other diverging path, and an unreachable exit, contributes no state. If no exit edge is reachable, the loop has no post-loop ownership state and diverges. The join is the same per-binding ownership join used for conditionals (3.8:50): a linear-carrying value must satisfy the consumption requirement on every reachable exit, while an affine, non-Copy move-type binding is MovedOut after the loop if any reachable exit has it MovedOut. Copy uses never move and therefore never create MovedOut; a Copy binding remains Owned. Loop-local bindings are not part of the post-loop state because their scopes end when the iteration exits.

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, a struct type with a field whose type carries a linear value, or an enum type any of whose variants has a payload component whose type carries a linear value (the payload join of 6.3:19 — the active variant is not known statically, so the type's worst case governs). 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 or constant-index element access that destructures a value whose struct type is declared linear (3.8:33) to implicitly drop any residue that carries a linear value. The selected smallest declared-linear place is checked recursively through nested fields and array elements: destructuring consumption extracts the accessed leaf and destroys droppable residue immediately, so a linear sibling or residual element would otherwise be silently dropped. Declared-linear ancestors are not destructured by that projection; their own obligations and unrelated residue remain subject to ordinary ownership checking. A whole-value custom destructor on the consumed declared-linear type also rejects projection (3.9:34), because no residue-safe whole-value destructor mechanism exists.

A field access on a struct that is linear only by infection (3.8:58) does not destructure it. It is an ordinary partial move of exactly the accessed field (3.8:22): sibling fields remain accessible (3.8:22 for move-typed siblings, 3.8:28 for Copy ones), and the non-moved residue is dropped by the ordinary scope-exit walk of the binding (3.9:2), or immediately by @drop of the partially moved place (3.9:37–38). Such a carrier's must-consume obligation (3.8:32) attaches to its linear sub-places rather than to the carrier as a whole: it is discharged by consuming each linear sub-place on every path, and a linear sub-place left unconsumed is reported where it is dropped — at scope exit or explicit @drop — not at a sibling's access. Consuming a linear sub-place therefore leaves the siblings, including any destructor they carry, to drop normally.

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: 'c.inner' is never consumed (reported at scope exit)
}

fn ok() -> i32 {
    let c = Container { inner: MustUse { value: 1 }, tag: 2 };
    sink(c.inner)    // OK: consumes the linear field; 'tag' (non-linear) is dropped
}

fn also_ok() -> i32 {
    let c = Container { inner: MustUse { value: 1 }, tag: 2 };
    let t = c.tag;   // OK: 'tag' is Copy, so reading it moves nothing (3.8:28)
    sink(c.inner) + t
}

The declared-linear case is the one this rule rejects at the access itself, because there the access really does drop the siblings:

linear struct MustUse { value: i32 }

linear struct Pair { inner: MustUse, tag: i32 }

fn main() -> i32 {
    let p = Pair { inner: MustUse { value: 1 }, tag: 2 };
    p.tag            // ERROR: destructuring 'p' would implicitly drop linear field 'inner'
}

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_use attributes)

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. The rule applies to a place at any depth — a field or constant-index element with a part moved out anywhere below it — and to passing it by reference as well as by value: a borrow or inout argument, a borrow self or inout self receiver (including an accessor's receiver, 6.6:8), and an equality operand (4.3:3f) are place contexts (3.8:76), but each loans the whole place, so the loaned place must be fully owned (core calculus docs/formal/01-core-calculus.md §5.4). Reinitializing the moved part (3.8:55) makes the place usable again; when the moved part lies below an array element, only the whole array can be reinitialized (3.8:72, 7.1:46), and doing so makes the place usable again.

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. The same holds for the target of an assignment: assigning to a place strictly below a moved ancestor (o.f.x = 1 after o.f was moved) is a compile-time error, because it would store into storage the variable no longer owns; only the moved place itself may be reinitialized (3.8:55). A place below a non-constant array index is accessed through the whole array that index selects from (3.8:70), so that array must be wholly owned (no element, and no part of one, moved out) and each of its ancestors must own its storage; a moved sibling of the array does not affect the access.

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
}

Assigning to a place whose type carries a linear value (3.8:57) is a compile-time error when the place currently holds a live value. A linear value MUST be consumed explicitly (3.8:32); an assignment that overwrote it would drop it implicitly (3.9:18), which linearity forbids. The assignment is legal only when the destination place has provably been moved out on every path reaching it — the reinitialization idiom (3.8:55): a moved-out place holds nothing to destroy. For an array whose element type carries a linear value, whole-array reassignment is legal only when every element has been consumed (as a whole or element-wise, 3.8:71); whole-array reinitialization is the only recovery path, because once any element has been moved out, assigning into the array — to an element or through an element — is itself an error (E0480, 3.8:72 and 7.1:46), including at the exact constant index that was moved out. An element reached through a non-constant (runtime) index can never be proven moved out and its assignment is always rejected. This restriction applies regardless of the run-time move state: the diagnostic is determined by the destination's type together with the statically tracked move paths, never by a run-time drop flag.

linear struct L { v: i32 }

fn remake(x: L) -> L { @drop(x); L { v: 9 } }

fn main() -> i32 {
    let mut x = L { v: 1 };
    x = L { v: 2 };   // ERROR: would overwrite a live linear value
    x = remake(x);    // OK: the right-hand side consumed x first
    @drop(x);
    0
}

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 consumed on every non-diverging path — moved out as a whole (a constant-index move, 3.8:68), or, for an element whose type is a carrier struct linear only by infection (3.8:58), by consuming each of the element's linear sub-places (3.8:60). The sub-place route applies to an array anywhere in a place tree: the elements of an array reached through a field projection cannot be moved out as wholes (3.8:68), but consuming their linear sub-places (for example h.arr[0].p and h.arr[1].p) still discharges the array field's obligation. Consuming only some elements, or an element on only some paths, is a compile-time error naming the elements — or the sub-place — left unconsumed.

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, including an empty marker struct. A projection from such a struct therefore consumes the struct value and must either destroy only legally droppable residue or be rejected.

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
}