Skip to content

capsule<T>

Capsule<T> is a compiler-reserved special type, like Unique<T>. It is a move-only, isolated owner: it wraps a single value whose isolation the compiler can fully account for, so the value can be moved — and sent across a channel — without smuggling shared or aliased state along with it.

Contract

Capsule(value) moves value into an isolated owner. The capsule itself does not allocate; allocation behavior belongs to the payload type. Capsule<T> is move-only and can cross channel worker boundaries only when T satisfies the compiler's isolation rules.

The payload can be observed or mutated only through scoped callbacks. Callback errors are propagated by .with and .update; the capsule operations do not introduce their own error set.

Constructing

Zynx
let c = Capsule(42);                       // Capsule<i32>
let s = Capsule(try ("hi").clone());       // Capsule<^str>

Capsule(value) moves value into the capsule. The source is consumed:

Zynx
let s = try ("hi").clone();
let c = Capsule(s);
_ = s;   // error: use of moved value `s`

Capsule<T> is move-only (never Copy) and has the same in-memory representation as T (the payload is stored inline).

Isolation rules

T must be isolatable. Allowed:

  • scalars (integers, floats, bool), enums, tuples, fixed arrays
  • plain owned structs (all fields isolatable, no user-defined drop)
  • the compiler-recognized owned containers ^str and ^[T] when their element types are isolatable

Rejected:

  • checked references &T and raw pointers *T
  • unqualified Future<T>, Future<T> where Send, scoped Join, channel and other runtime handles
  • erased callables / closures
  • user-defined drop types
Zynx
let p: *i32 = null;
let c = Capsule(p);   // error: `*i32` cannot be placed in a capsule

Channel send-safety

A Capsule<T> is send-safe exactly when T passes the isolation check above. This makes it the way to move an owned container across worker threads even when the bare container is not itself send-safe:

Zynx
let (tx, rx) = channel.bounded<Capsule<^str>>(1);     // ok
await tx.send(Capsule(try ("hi").clone()));           // ok

let (tx2, rx2) = channel.bounded<^str>(1);
await tx2.send(try ("hi").clone());   // error: cannot move value of type `^str`
                                      // across worker threads

Channel payload cleanup may drop a Capsule<T> on the receiving/runtime owner side; this is sound because only compiler-recognized isolatable/drop behaviour is ever placed in a capsule.

Scoped access (.with / .update)

Use .with for read-only scoped access to the payload:

Zynx
var c = Capsule(State { count: 10 });

let n = c.with((state: State) -> i32 {
    return state.count;
});

Use .update for scoped mutable access:

Zynx
let next = c.update((state: &State) -> i32 {
    state.count = state.count + 1;
    return state.count;
});

The callback result is the result of the method call. If the callback throws, the method call throws the same error set and must be handled with try or catch.

Zynx
let value = try c.with((state: State) throws(ReadError) -> i32 {
    if state.count < 0 {
        throw ReadError.Invalid;
    }
    return state.count;
});

The access reference is only valid during the callback. .with requires a T callback parameter; .update requires &T. Callbacks must use an explicit closure return type so the compiler can infer the method result type.

Use std.channel for cross-worker movement of send-safe payloads and std.async for the runtime boundaries that make send safety matter. Use the language Ownership chapter for the broader ownership model.

Status

Capsule<T> is a current public compiler-reserved ownership type documented in the standard-library section because it is commonly used with std.channel. The isolation accept/reject list above is the current public contract.

Released under the MIT License.