Bindings, moves, and copies
Bindings and mutable reassignment follow the declaration and assignment rules. Ordinary value use (assignment, owned argument passing, and return) either copies or transfers ownership according to the value’s type:
Numbers, bool, str, bytes, ranges, and named function values copy. A
tuple, Option, Result, enum, or anonymous sum (A | B) copies when all of
its parts copy. A class copies only when it is declared @copy, and a generic one only when its type arguments
also copy.
Every other value moves: collections, boxes, closures that capture,
generators, and other class instances.
A class of integers is still affine unless declared @copy. Copied strings may
share immutable storage. Inserting an affine value into a collection or
capturing it transfers ownership.
A moved-from binding cannot be read until a permitted reassignment initializes it.
copy(value) observes its argument and returns Result[T, AllocError] containing
an independent value. It recursively duplicates owned mutable contents;
immutable strings can share storage. A nested
Shared handle is not duplicated:
the copy holds another owner of the same content, as share() would.
copy(handle) on a handle itself is rejected in favour of handle.share().
Copying does not supply mutable aliases.
For a value that already copies, copy(value)? equals value; checking
reports it as a redundant-copy warning.
Generators, captured closures, recursive data, and types containing custom
destruction cannot be copied. Allocation
and destruction describe failure and cleanup.
Only paths that continue contribute to availability at a branch join. Moving on
some continuing paths makes a binding possibly moved; moving on all makes it
moved. Reinitializing a mutable binding restores availability. Every loop
backedge, including continue, must preserve outer owners available at loop
entry. Moving and then reinitializing is accepted; carrying the move to a
backedge is rejected. Break paths join the zero-iteration path.
Moving fields
An affine field can move out of a class instance held by an owned binding,
including an owned self receiver. The path from the binding to the field may
pass through further class fields, as in outer.inner.resource. Availability
is then tracked per field, with the same rules as bindings:
- The moved field, and any place inside it, cannot be used until it is assigned.
- The other fields remain usable and can move out too.
- The binding cannot be used as a whole while a field is moved: it cannot be
moved, passed, returned, borrowed, or used as a method receiver. Assigning
the field (
value.field = replacement) or the whole binding restores it. - At a branch join, a field moved on some paths is possibly moved. A field moved in a loop must be assigned again before the backedge.
- When the binding goes out of scope, or a path returns early, only the fields still initialized are destroyed, each exactly once.
class Resource:
name: str
def __del__(self) -> ():
print("close", self.name).unwrap()
class Pair:
left: Resource
right: Resource
def consume(resource: Resource) -> ():
print("consume", resource.name).unwrap()
def main() -> ():
mut pair = Pair(Resource("a"), Resource("b"))
consume(pair.left)
print(pair.right.name).unwrap()
pair.left = Resource("c")
whole = pair
print("end").unwrap()
consume a
close a
b
end
close c
close b
Using the binding as a whole before the field is assigned is rejected:
class Resource:
name: str
class Pair:
left: Resource
right: Resource
def main() -> ():
pair = Pair(Resource("a"), Resource("b"))
left = pair.left
whole = pair
use of partly moved binding `pair`: `pair.left` is moved; assign `pair.left` before using `pair` as a whole
Moving out needs no mut binding, as moving the whole value does not, but
assigning the field back does. Fields cannot move out:
- through a reference, including bare
selfand&mutreceivers; usereplace, which transfers the old value while leaving the field initialized; - of a class that defines
__del__, which receives the whole object and so needs every field initialized;replaceworks there too; - through a
Box, aSharedhandle, a collection element, a closure capture, or a temporary.
Tuple elements move by unpacking the whole tuple, and enum payloads by
consuming match. Use an explicit copy or a collection removal operation for
collection elements. Two list elements can be exchanged in place with
swap.