Move Semantics

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

Value Categories

Types in Gruel 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 @mark(copy) Directive

A struct or enum type's posture MAY be declared with an @mark(copy) directive on the type's declaration head (ADR-0083). Under uniform structural inference, an unmarked type whose members are all Copy already infers Copy posture; @mark(copy) is therefore an assertion that the type's members keep it Copy and turns a silent posture downgrade (e.g. adding a non-Copy field later) into a declaration-site error.

mark_directive = "@mark" "(" marker_name ("," marker_name)* ")" ;
marker_name    = "copy" | "affine" | "linear" ;

A struct or enum that infers Copy posture (or that carries @mark(copy)) is a Copy type. Using a Copy value does not consume it; the value is implicitly duplicated.

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

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 type marked @mark(copy) MUST contain only members (fields, variant payloads) that are themselves Copy types. It is a compile-time error if any member has a type that is affine or linear.

struct Inner { name: String }              // infers Affine (String is Affine)

@mark(copy) struct Outer { inner: Inner }  // ERROR: field 'inner' is affine

A type that infers Copy (or asserts Copy via @mark(copy)) MAY contain members of primitive Copy types (integers, booleans, unit), other Copy types, tuples of Copy elements, or arrays of Copy elements (ADR-0083 Open Question 1). Vec(T) is perpetually non-Copy because it owns heap memory and conforms to Drop.

struct Point { x: i32, y: i32 }                       // infers Copy
struct Rect { top_left: Point, bottom_right: Point }  // infers 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 or enum type MAY be declared as a linear type with an @mark(linear) directive on the type's declaration head. A type whose members include a Linear field also infers Linear automatically (linear is contagious upward, ADR-0083).

mark_linear = "@mark" "(" "linear" ")" ;

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)
  • Destructured via a let destructuring binding (all fields are transferred)
  • A field is accessed (the linear value is consumed by the access)
@mark(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.

@mark(linear) struct MustUse { value: i32 }

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

A @mark(linear) struct or enum MUST NOT also carry @mark(copy). The two posture markers are mutually exclusive whether they appear in one directive or in two — the validator collects all markers from all @mark(...) directives on an item, deduplicates by name, and rejects the combination.

@mark(copy) @mark(linear) struct Invalid { value: i32 }  // ERROR

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 Interface

Handle is a compiler-recognized structural interface (ADR-0075). A type conforms to Handle when it provides a method named handle with the signature fn handle(self: Ref(Self)) -> Self. Conformance enables explicit duplication via a .handle() call: the receiver is borrowed and a freshly owned value is returned.

There is no directive for Handle. The interface is opted into by defining the conforming method directly on the struct or enum:

handle_method = "fn" "handle" "(" "borrow" "self" ")" "->" "Self" block ;

A method named handle with the receiver mode self: Ref(Self), no other parameters, and return type Self makes the host type conform to Handle. A method named handle with any other shape does NOT make the host type conform — it is an ordinary method, and @implements(T, Handle) returns false for that type.

It is a compile-time error to call .handle() on a value whose type does not have a handle method.

struct Counter {
    count: i32,

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

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

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. This is enforced by the self: Ref(Self) receiver: the type system treats .handle() as a borrow, not a move.

Handle is 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-like semantics: any Copy value can be duplicated by use, with no .handle() call required. Copy types are not required to conform to Handle.

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)

Handle shares its signature shape with Clone (both are fn name(self: Ref(Self)) -> Self); they remain distinct interfaces because Handle is permitted on linear types whereas Clone is not (§3.8:49).

A @mark(linear) struct MAY conform to Handle. Explicit duplication of a linear value is the canonical use case for Handle (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)

It is a compile-time error to move a non-Copy field out of a struct. To access non-Copy fields individually, the struct must be destructured using a let destructuring binding, which consumes the entire struct and binds all fields.

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

fn consume(i: Inner) -> i32 { i.x }

fn main() -> i32 {
    let s = S { a: Inner { x: 1 }, b: Inner { x: 2 } };
    consume(s.a)   // ERROR: cannot move field `a` out of `S`
}

This restriction eliminates partial moves — a value is either fully live or fully consumed. To access individual non-Copy fields, destructure the struct:

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

fn consume(i: Inner) -> i32 { i.x }

fn main() -> i32 {
    let S { a, b } = S { a: Inner { x: 1 }, b: Inner { x: 2 } };
    consume(a)   // OK: a is now an independent value
    // b is dropped at scope exit
}

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
}

Posture and Drop (ADR-0059, ADR-0080)

Gruel's three ownership postures (Copy, Affine, Linear) interact with the Drop interface (fn __drop(self)). Conformance to Drop is computed by the compiler — built-in types acquire conformance through synthetic rules, user types by defining the corresponding inline method.

For every struct or enum T:

  • T is Copy iff its declaration carries the copy keyword (or, for anonymous literals, every member is itself Copy). Every member of a Copy type MUST be Copy.
  • T conforms to Drop iff T is not linear and T is not Copy. Affine types always conform to Drop; their drop body is either user-written via fn __drop(self) (ADR-0053) or the compiler's recursive field-drop synthesis.

Copy and Drop are mutually exclusive: a single type MUST NOT be both. A copy declaration that also defines fn __drop(self) is rejected at the declaration site. Linear types are neither Copy nor Drop.

Generic code branches on posture via the @ownership(T) reflection intrinsic, typically combined with a comptime if guard:

fn use_copy(comptime T: type, t: T) -> i32 {
    comptime if @ownership(T) != Ownership::Copy {
        @compile_error("use_copy requires a Copy type");
    }
    // …
}

@implements(T, Iface) continues to drive interface conformance for real interfaces (Drop, Clone, Handle, Eq, Ord, user interfaces); ADR-0080 retired Copy from the interface set, so a Copy argument to @implements falls through the existing "unknown interface" diagnostic.

Clone Interface (ADR-0065)

Gruel exposes a third compiler-recognized structural interface, Clone (fn clone(self: Ref(Self)) -> Self). Clone formalizes "explicit deep duplication" for affine types and is the single conformance every collection method, generic constraint, and built-in clone helper resolves against.

For every type T:

  • T conforms to Clone iff T is not linear and any of the following holds:
    • T conforms to Copy (the synthesized clone is the bitwise copy);
    • T is a built-in type whose registered method set contains a clone method (e.g. String); or
    • T is a struct or enum that defines a method with the signature fn clone(self: Ref(Self)) -> Self (written inline, spliced via @derive(Clone), or hand-written in an extension block).

Linear types MUST NOT conform to Clone. The conformance check unconditionally rejects them; @derive(Clone) on a linear declaration is also rejected at the declaration site.

Generic code may constrain on Clone exactly as on Copy or Drop: fn duplicate(comptime T: Clone, x: T) -> T { x.clone() } accepts any type whose conformance check passes and rejects non-conforming types at the call site.

Canonical Option(T) (ADR-0065)

The compiler unconditionally registers a canonical Option(T) generic enum in every compilation. The definition is equivalent to

fn Option(comptime T: type) -> type {
    enum { Some(T), None }
}

and is available without any import or use directive. User code MUST NOT redefine the name Option; doing so is a duplicate-definition error at the redefinition site.

Option(T) flows through the standard enum-with-data machinery (§4.7). Pattern matching, exhaustiveness checks, and codegen do not special-case it. Future ADRs may introduce layout optimizations (e.g. null-pointer-as -None for Option(Ptr(T))); v1 ships the naive { tag, payload } layout.

Option(T) ships with the following methods, defined in the prelude:

  • fn is_some(self) -> bool — true iff the receiver is Some.
  • fn is_none(self) -> bool — true iff the receiver is None.
  • fn unwrap(self) -> T — returns the contained value, or panics if None.
  • fn unwrap_or(self, default: T) -> T — returns the contained value if Some, otherwise returns default.

Each method consumes the receiver. Because Option(T) is treated as Copy (§3.8:2 — all enum types), receivers are implicitly duplicated at the call site, so a name remains usable after a query method.

The companion canonical type Result(T, E) is documented in §3.13.