Ownership, Borrowing & Lifetimes
Jiang uses T^ for owned values and T& / T[]& for non-owning borrowed views.
This chapter explains how resources move, when they are destroyed, and how references avoid dangling.
Move and Destruction
Section titled “Move and Destruction”Movable is a default auto trait: values may change storage address unless their nominal type
explicitly opts out with !Movable. A !Movable value must be initialized directly in its final
place; it cannot be passed or returned by value, assigned to a new place, captured, or moved.
Direct Task<T> and Mutex<T> values use this rule to remain address-stable.
Copyable extends Movable and decides whether ordinary value use copies or moves. Scalars, enums,
shared references, raw pointers, and RawFn values are copyable. User-defined struct and payload enum
types are not copyable unless they explicitly implement Copyable and every field or payload is
also copyable. A type with a custom deinit cannot be Copyable.
Non-copyable but movable values move by default in assignments, arguments, returns, and captures:
The source is invalid after the move:
$.move() remains available as an explicit spelling. It can also force a move of a copyable value:
Declare copyable value types explicitly:
An ordinary resource-owning struct is movable but not copyable, so plain assignment transfers it:
Runtime destruction follows ownership, fields, payloads, and custom deinit; it is not enabled by
the Movable marker. Dropping T^ first drops the pointee and then frees its heap storage.
Tuple, array, optional, errorable, struct, and payload enum values recursively drop owned contents.
Non-owning views such as T&, T*, and T[]& do not free their targets. A custom deinit runs
before fields are dropped in reverse declaration order.
Reference Rules
Section titled “Reference Rules”References do not own their target. A reference must not outlive the value it points to:
Ordinary local declarations create references with .ref() or .mut_ref(). ref and ref! are
reserved for destructuring, match payloads, and lambda captures:
Inside optional and enum payload patterns, use ref name to borrow the payload instead of copying
or moving it. The omitted type position means _; explicit ref T name remains available:
Returning a reference to a local variable is not allowed:
Global variables can also be borrowed, but a global borrow does not get a special static lifetime. It is still an ordinary borrow that must stay within the current call stack:
Use public when a global value should be accessed from another module. Do not return a global
borrow just to make the value reachable.
When an API seems to need a static lifetime, prefer naming the global value directly or exposing a function that takes a fresh local borrow at each call site:
Return the value itself when you want a value:
Return T^ when you want to transfer ownership of a heap object:
T^ owns the allocation and the T stored in it. If T contains non-owning references, the owner
does not extend the lifetime of their sources; the resulting T^ retains those lifetime requirements.
Jiang distinguishes shared readonly and unique mutable loans. Any number of shared T& views may
coexist. A T& does not grant write capability through that reference, but an ordinary shared view
does not by itself freeze updates through the owner. A live T&! loan must be exclusive for every
overlapping place and cannot be implicitly copied. While such a unique loan is live, overlapping
source storage cannot be overwritten, moved, or dropped.
Reborrowing T&! as T& creates a frozen shared view. The original unique reference may still be
read, but it cannot be used to write until the shared reborrow’s last use. Several shared reborrows
may coexist.
Loan lifetimes are non-lexical: a loan ends after its last reachable use, so sequential mutable reborrows are allowed. Struct sibling fields and distinct constant array indexes can be borrowed independently; dynamic indexes and subslices conservatively overlap.
If an async call captures a borrow, its direct structured Task holds the loan until the Task has
finished and its scope cleanup is complete. cancel() only requests cancellation; use await() or
cancel_and_await() when the caller must wait for terminal completion. A heap Task that crosses a
scope or Domain must satisfy the normal lifetime and Sendable rules.
Cross-Domain transfer and lifetime validity are separate checks. A shared T& may be passed,
returned, or captured across Domains when T: Sendable, but it still cannot outlive its source
owner. T&! cannot cross directly; a call that needs T& may establish the frozen shared reborrow
described above. Raw pointers carry neither ownership nor lifetime proof and are not automatically
Sendable.
Lifetime Attributes
Section titled “Lifetime Attributes”Borrowing struct and payload enum types declare their public lifetime regions with @region. Every
declared region must be used by a field or enum payload. A reference has an outer borrow plus the
lifetime shape of its pointee. For a simple field, @life(a) binds the outer borrow and uses a for
every pointee lifetime as well:
Declaration order matters when the type is used with positional lifetime bindings. A clause such as
@region(a, b: a) declares both names and means a must live at least as long as b. Each target is
written once. Mutual constraints are allowed: @region(a: b, b: a) gives a and b the same
lifetime requirement.
Fields with several lifetimes can bind them in declaration order, as in @life(a, b), or by names
provided by the field type, as in @life(left: a, right: b). The two forms cannot be mixed. Every
name must be bound exactly once, and field bindings cannot use self.
A scalar source fills the complete lifetime shape of one top-level field or payload position. A
parenthesized scalar such as (a) means the same thing. Use a tuple source only when nested lifetime
positions need different sources. Every top-level position must still be present in the binding.
For example, if each Cell& position has two lifetime slots, these three payload bindings are
equivalent:
They are alternative spellings for one case declaration, not three attributes to write together.
For an exact binding of a reference whose pointee has several regions, keep its pointee shape grouped.
If PairRef has regions left and right, use @life(a, (left, right)); do not flatten it to
@life(a, left, right) or bind only one pointee region.
Generic regions name their source type explicitly. a: T means that a has the complete lifetime
shape of T; = anchor supplies a default for every slot when a field omits that generic binding:
Use @region(a: T) when a type stores T directly. A generic parameter must be used by a member
type; Jiang does not permit phantom nominal type parameters.
Functions and methods use target: source: a borrow named on the right must live long enough for
the value named on the left:
When a returned value contains a borrow and @life is omitted, a readonly self or Self&! self
receiver is the default source. Without such a receiver, the only parameter that carries borrowed
data becomes the default source. A parameter that carries several lifetimes still counts as one
parameter. This rule depends only on the function declaration, not on which branch the body returns:
A by-value Self self receiver has no priority and is treated like an ordinary parameter. If no
parameter carries borrowed data, write @life() to confirm that the result does not borrow from a
parameter. If several parameters carry borrowed data, or the source and result have different
lifetime positions, write @life explicitly. An explicit attribute defines all allowed lifetime
relationships; no default relationship is added. Each left-hand name may appear only once. Use &
when the result may borrow from either of several compatible sources:
Parameter-to-parameter relationships use the same syntax. @life(left: right) means every borrowed
part of right must live long enough for the corresponding borrowed part of left:
When a result carries several lifetimes, map the complete return once with a tuple expression:
Do not split that mapping into return.a and return.b, repeat the same left-hand name, or use
positional paths such as value[0]. Tuple elements and Fn / RawFn result and parameter positions
must be named before @life can refer to them. @life() explicitly states that there are no
lifetime relationships to parameters.
For example, a callable relationship uses its declared result and parameter names:
If there is no sufficiently long-lived input, return an owning value instead of a borrowed view.