Concurrency and capability types
Describes CruftScript's concurrency and capability types, Transferable, Capability, CapabilityBundle, DeepReadonly, Shared, and Compartment, which are meant to express at the type level what a compartment may hold, send, and share across threads. The runtime halves exist, but the checker does not enforce these types yet.
CruftScript runs code in compartments — isolated units of execution that can run on different threads and share almost nothing by default. This family of types is meant to describe, at the type level, what a compartment is allowed to hold, what it may send, and what it may share. Most of it is designed but not yet enforced, and the honest split between what runs and what does not is at the end of the page.
Transferable<T> // affine: sending one consumes the source binding
Capability<K> // an opaque, non-forgeable authority token
CapabilityBundle<…> // a tuple packaging several capabilities
DeepReadonly<T> // recursive, all-the-way-down immutability
Shared<T> // a deeply frozen value safe to share across threads
Compartment<C, M> // an isolated unit with capability row C and message type M
These types are the type-tier face of the memory and authority model described in Imogen: the shared-heap architecture and compartments and capabilities. The runtime keeps ordinary mutable objects local to one thread; only bytes and deeply-frozen values physically cross a boundary, and a compartment may hold only the authority it was granted. The types below are meant to make those runtime rules visible to the checker.
What each type means
Transferable<T> is affine, meaning use-once. When you transfer a binding, you consume it, and using that binding again afterward is a build-time error. This mirrors the runtime fact that a transferred value moves rather than copies: there is no second holder, so the type stops you from acting as if there were one.
const buf: Transferable<Bytes> = takeBuffer()
send(buf)
buf.length // error: buf was consumed by the transfer
Capability<K> is an opaque authority token. It is non-constructible and non-forgeable: you cannot write one out, only receive one that the host minted or that was handed to you. A compartment may only name capabilities that appear in its declared row, so code cannot reach for authority it was never granted.
CapabilityBundle<…> packages several capabilities together as a tuple, for handing a related set across a boundary in one piece.
DeepReadonly<T> projects a type to be immutable all the way down, not just at the top level. It recurses through the structure (terminating by structural decrease, the same discipline recursive types follow) and preserves optional modifiers as it goes.
Shared<T> is a value safe to hand to another thread. It requires the value to be DeepReadonly and rejects a cyclic graph. Both conditions come from the runtime: a shared value is deeply frozen and stored once, addressed by a handle and reclaimed by an atomic reference count, and that reclamation is only sound when the graph is acyclic. See Imogen for the arena that holds these values.
Compartment<C, M> types an isolated unit of execution. C is a structural row of the capabilities it holds, and M is the type of message it accepts. Its evaluate returns unknown, so you narrow the result before using it, and a value you send must be cloneable or explicitly transferred.
const c: Compartment<{ log: Capability<Log> }, Request> = spawn(...)
const out = c.evaluate(job) // out: unknown, narrow before use
Why it works this way
The runtime already guarantees that mutable state stays local, that only lawful values cross a boundary, and that a compartment holds only its granted authority. These types exist to lift those guarantees up to where the checker can see them, so a misuse shows up as a type error at build time instead of a failure at the boundary.
Affine transfer and non-forgeable capabilities have no real equivalent in TypeScript. Its unique symbol brands are a naming convention the checker does not police, so nothing stops you from using a "moved" value again or fabricating an authority token. The design here makes both into checked properties: a transferred binding is genuinely gone from the type system's view, and a capability is a value you can only ever have received.
Limitations
- The type-level rules are not enforced yet. This is the honest state: the six typing behaviors above are normative declarations that the live checker does not implement. Nothing today catches a use-after-transfer, a forged capability, or a non-
DeepReadonlyvalue passed asShared. The constructs are also not registered types, so writing one in a program —type T = Transferable<string>,Capability<...>,DeepReadonly<...>,Shared<...>,Compartment<C, M>— is an unresolved-type error today, not a silently-accepted annotation. - What the checker does enforce is narrower. Inside a
compartment { }block, a bare identifier resolves only if it is a constant, a local, a declared endowment, or an allowlisted intrinsic. That single scoping rule is live; the richer type guarantees are not. - The runtime halves exist; the type halves are contract-only. The frozen, atomically-reclaimed shared arena is real, and the boundary check that a compartment holds only capabilities in its declared set is real. Until the checker implements the rules above, treat the type-level guarantees as a contract the runtime keeps rather than something the types prove.