zena:ownership

zena
import {…} from 'zena:ownership';

Ownership and resource management — the layer 0 drop protocol.

See docs/design/ownership.md. This module is Track O's foundation: one interface that every kind of non-GC resource releases through, so that WIT handles, linear-memory allocations, WASI descriptors and FFI pointers share a single release mechanism instead of four.

Own<T>, Borrow<T> and Unmanaged<T> are the three handle kinds; disown and adopt move a resource between the affine regime and the manually-managed one. dispose is what all of them ultimately call.

Classes

Resource

zena
abstract resource class Resource implements OwnState
Implements OwnState

The classes declared with the resource modifier — those holding something the garbage collector cannot reclaim, and therefore carrying a release obligation.

resource class R satisfies this without an implements clause, the way a case class satisfies Hashable. It exists to be bounded against: disown and adopt take a T extends Resource rather than any T, because moving a value between the affine and manually-managed regimes is only meaningful for something that has a disposal obligation to move.

There is deliberately no way to hold one. Nothing is assignable to Resource, because the only reference to a resource a program can name is a handle — Own<R>, Borrow<R>, Unmanaged<R> — and those are nominally distinct types that assign to no supertype at all. That is what keeps a widening conversion from dropping the permissions the handle carries, and it is a property of the handles rather than of this interface: the same applies to every supertype, Resource or otherwise.

Resource-ness is what this names; affineness is what Own<T> declares. They are separate properties — see ownership.md §"Resource-ness and affineness are different properties" — so this bound restricts neither Own<T> itself nor the ordinary classes it will eventually wrap.

The root of every resource hierarchy. A resource class with no explicit superclass extends this one, so the lifecycle flag has an ordinary in-language home rather than a field codegen injects, and disown/adopt read it with an ordinary method call rather than an intrinsic. Being the root is also what makes ownership.md §"Resource-ness is inherited" cheap to satisfy: single inheritance means a resource cannot also extend an ordinary class, so the rule that its chain is all resources is structural rather than a check.

Declaring no method that returns this is load-bearing, not incidental. That is the whole reason an arbitrary superclass is unsafe for a resource and this one is not.

ResourceStateError

zena
class ResourceStateError extends Error
Extends Error

Raised when a regime change is handed a resource in the wrong state: adopting one that was never disowned, or disowning one twice.

Both are programming errors rather than expected conditions — the resource was already claimed by someone else, or already released — so they throw rather than returning a Result. A tryAdopt handing back the branch can be added if a caller ever wants it.

Constructors
zena
new(operation: String, actual: String, expected: String)
#

Interfaces

Disposable

zena
interface Disposable

A value holding something the garbage collector cannot reclaim.

dispose is symbol-keyed rather than name-keyed. dispose is a common enough method name that a class may already have one meaning something unrelated, and this is a protocol the language itself invokes implicitly — a silent collision would be a resource released at the wrong time. Call it as value.[Disposable.dispose]().

zena
import { Disposable } from 'zena:ownership';

class Descriptor implements Disposable {
  #handle: i32;
  new(this.#handle);
  [Disposable.dispose](): void { descriptorDrop(this.#handle); }
}

Disposal must be idempotent: dispose() may be called on an already disposed value and must not release the underlying resource twice. Until affine checking lands in O2, nothing statically prevents a double call, and a resource wrapper's state flag is what makes the second one a no-op.

Type aliases

Own

zena
type Own<T> = T

The owning reference to a resource. Affine: it may be moved, returned or stored, but not duplicated, and it is released when it leaves scope unmoved.

Erased at codegen: a distinct type is nominally distinct for checking but shares its target's representation, so a handle costs no allocation and no indirection. The three differ in permissions, not data.

Own<T> is what declares affineness; resource class declares resource-ness. The two are separate on purpose — see ownership.md §"Resource-ness and affineness are different properties" — which is why Own<T> will also apply to ordinary classes (from a provably exclusive source) once layer 4 needs it.

Borrow

zena
type Borrow<T> = T

A borrowed reference. Freely copied, but second-class: it may not be returned (except derived from a single borrow parameter), stored in a field or array, or captured by a closure. Never releases what it points at.

At unrestricted instantiations Borrow<T> is the identity — Borrow<i32> is i32 — which is what keeps container APIs from forking.

Unmanaged

zena
type Unmanaged<T> = T

A disowned resource: aliasable like an ordinary reference, and never implicitly dropped. You must dispose() it yourself or adopt() it back into the affine world.

This is the population using was designed for. Entering it costs leak-freedom and compile-time use-after-dispose detection; it costs neither type soundness nor memory safety, because the state flag turns a bad adopt into a clean error rather than a double free.

Scoped

zena
type Scoped<T> = T

A value that may not be duplicated and may not outlive the extent it derives from — the corner of the universe table where both restrictions bind (ownership.md §"Scoped: the fourth corner"). The population is borrow-derived futures and iterators: an async function that holds a Borrow across await returns Scoped<Future<T>>, so the frame holding the borrow cannot escape the borrow's extent.

A scoped value must be consumed exactly once on every path: a scoped future by await or a move, a scoped iterator by for/in. It may not be stored in a field, container, record, or tuple, and may not be captured by a closure. A first-class Future<T> coerces to Scoped<Future<T>>; there is no way back.

Functions

disown

zena
function disown<T extends Resource>(value: Own<T>): Unmanaged<T>

Leaves the affine regime: takes the owning handle and hands back an aliasable one that is never implicitly dropped.

zena
let raw: Unmanaged<Descriptor> = disown(f);   // f is consumed
// … alias it, store it in a field, put it in an ordinary Array …
let f2: Own<Descriptor> = adopt(raw);         // back under implicit drop

The handles are erased, so this re-types the object rather than allocating anything: Own<T> and Unmanaged<T> lower to the same wasm type, and the body compiles to an identity function returning its argument. The call itself is not yet inlined away.

Requires the resource to be owned and leaves it disowned, so disowning the same resource twice throws rather than handing out two unmanaged aliases with one disposal obligation between them.

disown consumes its argument. Otherwise an Own that will implicitly drop would coexist with an Unmanaged alias to the same resource, and the drop would run under an alias that outlives it. Move checking enforces the consumption for local bindings — a caller that keeps using the original Own after disowning it gets a compile error, through the ordinary rule that an argument to an Own parameter is a move. The flag still guards the routes the checker does not track, such as an Own read out of a field.

The cast is legal here and nowhere else: Own<T> and Unmanaged<T> are opaque types declared in this file, and both erase to T, so re-labelling one as the other mints nothing. See ownership.md §"Handles are not forgeable" — outside this file the same cast is a diagnostic, which is what makes disown and adopt the only doors between the two regimes.

adopt

zena
function adopt<T extends Resource>(value: Unmanaged<T>): Own<T>

Re-enters the affine regime: takes a disowned handle back under implicit drop, so it is released at scope exit again.

The inverse of disown, and the only way back — Unmanaged<R> has no user-callable dispose, since it is not an owner and cannot call the consuming form. A disowned resource is either scoped with using or adopted back.

Throws when the resource is not in the disowned state: owned means something else adopted it first, so two racing adopters do not both succeed — the loser gets a clear error rather than a second owner and, once implicit drop lands, a double free.

dropped throws too: every consuming dispose sets it, so adopting an already-released resource is reported as exactly that.

adopt cannot retract aliases. Aliasability is the point of the disowned state, so the flag is the only defence here, by design.