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]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 @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
letdestructuring 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_useattributes)
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:
Copytypes are duplicated implicitly when usedHandletypes require explicit.handle()calls for duplicationCopyis appropriate for small, cheap-to-copy types (likePoint)Handleis 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:
Tis Copy iff its declaration carries thecopykeyword (or, for anonymous literals, every member is itself Copy). Every member of a Copy type MUST be Copy.Tconforms toDropiffTis notlinearandTis not Copy. Affine types always conform toDrop; their drop body is either user-written viafn __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:
Tconforms toCloneiffTis notlinearand any of the following holds:Tconforms toCopy(the synthesizedcloneis the bitwise copy);Tis a built-in type whose registered method set contains aclonemethod (e.g.String); orTis a struct or enum that defines a method with the signaturefn 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 isSome.fn is_none(self) -> bool— true iff the receiver isNone.fn unwrap(self) -> T— returns the contained value, or panics ifNone.fn unwrap_or(self, default: T) -> T— returns the contained value ifSome, otherwise returnsdefault.
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.