Move semantics
move source explicitly transfers a local or field value and its destruction responsibility. The source place stops containing a live value; ordinary syntax continues to mean copy.
Copy, move and reference
Vector3 copy = value; // value and copy remain live
Vector3 transferred = move value; // value is now moved
Vector3& reference = transferred; // borrow the same storage
move is a destructive value transfer, not a promise that the compiler copies zero bytes. The compiler may use registers, loads and stores, bulk copies, or eliminate the relocation. The language guarantee is that only the destination remains logically live.
References do not copy or transfer lifetime responsibility. Applying move to a reference binding is rejected. A known reference to a place is valid only while that place contains a live value; using the reference after the source is moved is a compile-time error.
Relocation
Move is the language-level transfer of a value's lifetime and destruction responsibility. Relocation is the physical operation the compiler uses to implement that transfer.
Move = language-level lifetime transfer
Relocation = physical implementation of that transfer
Relocation may use registers, loads and stores, memcpy, recursive field or element relocation, an ownership-handle transfer, array backing-data relocation, a return slot, or complete relocation elision. For example, LargeStruct b = move a; may physically copy the struct representation. It is still a move because a becomes Moved, b is the only live destination, and a is not destroyed.
Copyability
Copyability is separate from movability and destruction. A struct is copyable only when its base and every inline field are copyable. A concrete generic type determines this after substituting its type arguments. A generic type parameter itself is not guaranteed to be copyable, regardless of its constraints.
Pair first = Pair(1, 2);
Pair second = first; // recursive field copy; both remain live
second.Left = 9; // first.Left is still 1
A named local, parameter, field, or indexed place used in a by-value initializer, argument, assignment, or return is a copy source unless it is wrapped in move. Fresh constructor and function results transfer naturally into their destination and do not create a second logical owner.
Ownership-producing expressions must have a destination or consumer. A standalone move value;, lock observer;, or new Resource(); is rejected because it would discard the ownership result. Initializers, assignments, returns, call arguments, and type-correct larger expressions consume those expressions normally. Raw malloc/calloc results likewise need a destination. In contrast, destruct(value);, free(pointer); and delete(pointer); are standalone lifetime/memory operations and do not produce values.
A destructible temporary nested anywhere in a full expression remains alive until that expression ends unless its ownership is transferred earlier. Xenon then cleans up all remaining temporaries exactly once in reverse construction/evaluation order. Thus GetShared()->Use() keeps its shared receiver alive during the call, while a value transferred into a destination or consuming parameter does not receive duplicate temporary cleanup.
A composite copy includes nested and inherited struct values. Plain fields use their normal value copy. A shared<T> field retains one strong owner and a weak<T> field retains one observer. The unique<T> type is non-copyable, so a containing concrete struct also becomes non-copyable transitively.
An open generic such as Box<T> is not guaranteed to be copyable when its fields depend on T. Generic constraints do not make the parameter copyable for value-copy analysis. A generic declaration that requires such a copy is rejected even when it has no concrete instantiation. Use an explicit move when the declaration only transfers its argument:
T Transfer<T>(T value)
{
return move value; // valid for copyable and non-copyable T
}
A raw pointer or user-written destructor does not by itself make a struct non-copyable. Such a copy duplicates its fields and creates two live values, so shallow resource handles may still be released twice. Use move when destruction responsibility must be transferred instead of duplicated.
Source places
move accepts writable, initialized scalar locals, by-value parameters, and tracked field paths rooted in them or in the current this object. A temporary already transfers naturally into its destination, so moving a literal or call result is rejected.
Buffer first = Buffer(1024);
Buffer second = move first; // valid
// Rejected: first has been moved.
// first.Use();
// Rejected: a temporary is not a source place.
// Buffer third = move Buffer(256);
unique<Resource> Take()
{
return move this.resource;
}
Field projections can be nested, such as state.transform.position and this.state.resource. A moved this field can be restored by assignment, after which the complete current object is usable again. The same user-destructor restriction applies to fields rooted at this. Whole move this is not supported.
A raw T[] is a normal movable value. Heap arrays relocate their handle. A stack array can transfer within the same scope or to an enclosing local in the same function; the compiler may retain or relocate its backing storage to match the destination lifetime, without hidden heap promotion.
Callable move effects
Moving a receiver field changes caller-visible semantic state. The compiler records this as a method effect rather than relying on the old bytes being overwritten with null or a default value:
unique<Resource> Take()
{
return move value;
}
unique<Resource> resource = holder.Take();
// holder.value is Moved; holder is PartiallyMoved
A direct call applies that effect to the receiver place in the caller. Unrelated sibling fields remain usable under the normal partial-move rules. Assigning a new value to the moved field restores it.
Receiver effects are part of the callable contract. An interface implementation, virtual method, or override cannot introduce a persistent receiver move effect that is absent from the contract visible to callers. Such a declaration is rejected instead of hiding the state change from callers using the interface or base contract.
The effect of the implementation selected by interface or virtual dispatch must match the contract visible at the call site. Consequently an effectful method inherited from a base or multi-level base cannot silently satisfy an effect-free interface, and concrete generic methods obey the same rule.
Stable state at every exit
Every reachable method exit must leave each caller-visible receiver place in the same move state. An effect is recorded only when the field is moved on every exit. This method is invalid because value is moved on only one path:
unique<Resource> Take(bool consume)
{
if (consume)
return move value;
return new Resource();
}
A callable must leave the same persistent receiver-move state at every exit. Xenon therefore rejects the conditional move above. Moving a field and restoring it before every exit is valid and produces no persistent receiver move effect.
Partial and nested moves
State state = CreateState();
Resource resource = move state.resource;
state.count++; // valid: sibling field is live
// Use(state); // error: state is partial
// state.resource.Use(); // error: resource was moved
state.resource = Resource(); // restores this field
Use(state); // valid again
The flow model tracks a root local or by-value parameter plus its field projections. Moving a field marks that exact place moved and its ancestors partially moved. Unmoved sibling fields remain usable. A complete read, by-value argument, copy or method call on a partially moved ancestor is rejected because it could observe the missing field.
Assigning a new value to the moved field restores that field and its nested subtree. Assigning the whole root restores every field. At control-flow joins, a place is considered moved when it may be moved on any continuing path.
A field cannot be moved out or explicitly destroyed through any aggregate ancestor with an explicit destructor. Such a destructor may require the complete value. Move or destroy the whole aggregate instead; the same boundary applies through resolvable local and returned references.
Initializers, assignments and arguments
A move can initialize a new local, fill an uninitialized local in another scope, overwrite a live local, or provide a by-value function argument.
Buffer destination;
{
Buffer source = Buffer(1024);
destination = move source;
} // source cleanup is skipped
Consume(move destination);
When the destination is already live, Xenon first evaluates the source value, destroys the destination's old value, stores the transferred value, then marks the destination live. A self-move is rejected.
int[] values = int[10];
int[] other = move values;
int[] result;
{
int[] temporary = int[10];
result = move temporary;
} // result remains live; no heap promotion
For an inner-to-outer stack-array move, relocation transfers cleanup responsibility and gives the backing data the destination's lifetime. Trivial elements can use bulk byte relocation or retain the existing backing; destructible elements keep exactly one live cleanup registration.
Plain struct assignment uses the same safe overwrite sequence: evaluate the source copy, destroy a live old destination, then store the new value. Assigning a value to itself is recognized and does not destroy it.
Reinitializing a moved place
A plain assignment creates a new live value in moved local or field storage. No destructor runs for the old moved-out bits.
Buffer second = move first;
first = Buffer(256);
first.Use(); // valid again
Flow joins are conservative. If a branch may move a local, complete use after the branch is rejected unless every path leaves it initialized. Runtime cleanup state ensures that exactly the live owner is destroyed.
Returning ownership
Buffer MakeBuffer()
{
Buffer result = Buffer(1024);
return move result;
}
The return value is captured before scope cleanup. Because result is moved, its local destructor is skipped. The receiving value later runs the destructor exactly once. This supports manual RAII structs whose destructor releases owned raw storage with free or typed allocations with delete.
Returning stack-backed runtime-sized values
// Invalid: the backing storage belongs to the callee frame.
int[] Create(int count)
{
int[] values = int[count];
return move values;
}
A stack-backed array cannot cross a function return boundary because its backing storage belongs to the callee frame. Moving its value would produce a dangling array after the frame disappears. Allocating replacement storage in the same callee frame has the same problem, and explicit stack storage is not promoted to the heap.
The same lifetime rule applies to constant-length and runtime-length stack arrays.
// Valid: heap backing survives the callee frame.
int[] CreateHeap(int count)
{
int[] values = new int[count];
return move values;
}
// Valid: the caller owns the storage.
void Fill(int[] output)
{
output[0] = 42;
}
unique<T[]> and shared<T[]> may also be return-moved by transferring ownership; a shared move does not add a retain.
Ownership types
unique<T> is non-copyable single ownership. shared<T> and weak<T> provide strong and observing reference-counted handles. These ownership wrappers are distinct from move: plain RAII structs and ordinary arrays can move without becoming ownership wrappers.