Move, Copy, and Clone

Ownership moves, copy markers, explicit clone APIs, borrowing, RAII, and drops.

Zynx tracks ownership at compile time. Passing, assigning, returning, awaiting, or storing a move-only value transfers ownership; using the old binding after that transfer is a compile-time error.

struct Resource {
    id: i32,
}

fn consume(resource: Resource) -> i32 {
    return resource.id;
}

fn main() {
    let resource = Resource { id: 42 };
    let id = consume(resource);

    assert id == 42;
}

The call to consume(resource) moves resource. Reading resource.id after the call would be rejected.

Copy Values

Copy values stay usable after assignment or by-value calls.

Copy by defaultNotes
numeric scalarsintegers and floating-point values
bool, str, nullstring literals are borrowed immutable data
enumsenum values are copyable
raw pointers*T and const *T are low-level address values
SIMD vectorsvector<T, N> values are copyable
nullable Copy valuesT? is Copy when T is Copy

User structs are move-only unless they explicitly implement the Copy marker.

struct Point: Copy {
    x: i32,
    y: i32,
}

fn take(point: Point) -> i32 {
    return point.x + point.y;
}

fn main() {
    let point = Point { x: 3, y: 4 };
    let alias = point;
    let total = take(point);

    assert point.x == 3;
    assert alias.y == 4;
    assert total == 7;
}

A Copy struct cannot declare drop, and every field must also be Copy. Arrays, slices, references, owned strings, callable values, Unique<T>, error unions, and ordinary resource-owning structs are move-only.

Generic APIs can require copyable values with T: Copy:

fn dup<T: Copy>(value: T) -> T {
    return value;
}

fn main() {
    assert dup(9) == 9;
    assert dup("hello") == "hello";
}

See Interfaces for builtin marker interfaces.

Move Values

Plain structs, arrays and slices, references, callable values, error unions, Unique<T>, future.Future<T>, owned strings, sockets, channels, and most handles are move-checked.

struct Box {
    value: i32,
}

fn main() {
    let first = Box { value: 7 };
    let second = first;

    assert second.value == 7;
}

The assignment to second moves first. A later use of first would be rejected.

Moves are conservative in loops. Moving a value inside a loop is rejected when the next iteration could see the value as already moved.

References

&T is a mutable reference. const &T is a read-only reference. Assigning to a mutable reference writes through the reference.

fn read(value: const &i32) -> i32 {
    return *value;
}

fn set_to_two(value: &i32) {
    value = 2;
}

fn main() {
    let count = 1;

    assert read(&count) == 1;
    set_to_two(&count);
    assert count == 2;
}

Ordinary function and callable arguments do not create reference arguments implicitly. When a parameter expects &T or const &T, pass an explicit borrow such as &value, or pass an expression that is already reference-valued. Method receivers keep receiver auto-borrowing for self: &T ergonomics, and operator overload operands keep receiver-style borrowing because operator syntax has no explicit argument list.

Mutable borrows are exclusive. While a mutable borrow is live, the owner cannot be mutably borrowed again, immutably borrowed, moved, or reassigned. Borrowing a field or array element also protects the owning aggregate from whole-value reassignment.

Borrow scopes end when the reference binding leaves scope:

fn overwrite(value: &i32) {
    value = 9;
}

fn main() {
    let value = 1;

    if true {
        let borrowed: &i32 = &value;
        borrowed = 5;
    }

    overwrite(&value);
    assert value == 9;
}

In async fn, a mutable borrow may not live across await or select. End it before the suspension point, usually by using a smaller block.

Clone

Clone is explicit API, not automatic language behavior. Owned standard-library types expose clone-style methods when independent storage is meaningful.

import {
    String
} from std.string;

fn main() {
    let text = try String("hello");
    let copy = try text.clone();

    assert text.str() == "hello";
    assert copy.str() == "hello";
}

Owned constructors and clone helpers can allocate, so they often return error unions. String.clone("text") remains available as a legacy alias, but constructor syntax is the preferred way to create a new owned string from str.

Array<T> can store move-only element types, but methods that duplicate storage or values require T: Copy. See Collections.

Drops

Types can declare drop(self) to release owned resources. Drops run on scope exit, reassignment of an owning binding, and explicit std.drop(value).

import std.os;

struct Tracer {
    id: i32,

    drop(self) {
        _ = os.write("drop {self.id}\n");
    }
}

fn main() {
    let first = Tracer { id: 1 };
    let second = first;

    _ = os.write("mid {second.id}\n");
}

The value is dropped once, when second leaves scope. Moving transfers the eventual drop to the new owner; the moved-from binding is not dropped.

When a struct owns fields that also drop, the outer drop(self) body runs first, then owned fields are dropped.

The destructor syntax is the special struct member drop(self) { ... }, not a normal fn drop(self) method. See RAII and Drop for cleanup order, defer, with, and manual std.drop.

Common Transfers

Ownership moves happen in more places than assignment:

  • passing a move-only value to a by-value parameter
  • returning a move-only value
  • capturing a move-only value in a closure
  • assigning a callable value to another binding
  • awaiting, spawning, detaching, grouping, racing, or timing out a future handle
  • sending values through async/channel APIs
  • wrapping a move-only value with Unique(value)

See Closures and Captures, Async / Await, and Memory Model for deeper ownership interactions.