Skip to content
You are reading v0.1, which is not the current release. Go to v0.4.

Memory and resources

Khora manages ordinary memory automatically. Resource lifetimes that have external meaning—files, sockets, transactions, foreign handles—use explicit structured cleanup.

There is no source-level borrow or lifetime syntax for ordinary Khora values. The runtime uses reference counting and the compiler may remove retain/release operations or reuse uniquely owned storage when doing so cannot change program meaning.

The programmer-visible rules are:

  • a value remains valid according to ordinary lexical and type semantics;
  • optimization cannot make behavior depend on whether storage was reused;
  • writable state does not silently become cross-fiber shared state;
  • external resources require their own cleanup policy.

The primitive resource lifetime object is Region:

pub type Region;
impl Region {
pub fn open() -> Region;
pub fn root() -> Region;
pub fn defer(self, finalizer: () -> ()) -> ();
}

Open a region and register finalizers with defer:

fn work() -> Int {
let region = Region::open();
Region::defer(region, fn () => release_second());
Region::defer(region, fn () => release_first());
42
}

Finalizers execute in reverse registration order when the region is released.

Region::root() refers to the outer program region. Its finalizers run as the program exits.

The standard structured-resource capability is:

pub effect Scope {
defer: (() -> ()) -> (),
}

scoped creates a fresh region, installs a Scope handler over the body, and removes that capability from the caller’s required row:

pub fn scoped<A, 'ef, 'er>(
body: () -> A with { 'ef | scope: Scope } raises 'er
) -> A
with 'ef
raises 'er

Example:

fn inside() -> Int
with { scope: Scope }
{
scope.defer(fn () => cleanup());
7
}
fn outside() -> Int {
scoped(inside)
}

A named function is the normal argument to scoped when that function requires the scope capability.

Pass a named function to scoped where that function is the one receiving the scope capability. It keeps the capability passing explicit, and it is what lets row subtraction take scope back out of the caller’s row.

acquire registers a release operation and returns the acquired value:

pub fn acquire<A, 'ef>(value: A, release: (A) -> ()) -> A
with { 'ef | scope: Scope }

Typical form:

fn use_connection() -> ResultValue
with { scope: Scope }
{
let connection = acquire(open_connection(), close_connection);
query(connection)
}

release(value) runs when the enclosing resource scope ends.

A region is released on every structured path out of its owner:

pub type WorkError = | Failed;
fn work(fail: Bool) -> Int raises WorkError {
let region = Region::open();
Region::defer(region, fn () => cleanup());
if fail {
raise WorkError::Failed
}
return 1;
}

The finalizer runs on the normal path, the explicit return, and when the raise leaves the function.

Cancellation uses the same structured unwind path. A pending cancellation observed at a cancellation point releases intervening regions and runs their finalizers before the fiber terminates.

A catch handles failures in a raises row. Cancellation is not a failure variant and is not consumed by catch.

Cancellation is not an exception that can arrive between arbitrary instructions: a pending one is observed at a cancellation or failure propagation point, such as !. That is what keeps the code between two marked points readable as ordinary straight-line code, while still letting blocked or suspended work be woken so it can unwind and release what it holds.

The invariant a resource abstraction should be built on is the short one: if the scope ends, cleanup runs.

A resource-owning API should generally keep the lifetime inside one call:

fn with_resource<A, 'ef, 'er>(
body: (Resource) -> A with 'ef raises 'er
) -> A
with 'ef
raises 'er

rather than returning an unmanaged handle that callers must remember to close on every path.

A database transaction is this shape with a richer finalizer policy, and it belongs in the transaction abstraction rather than in every caller:

  • completing normally commits;
  • a typed failure rolls back;
  • cancellation rolls back before the connection returns to its pool.

Database transactions is that policy written out.

Foreign or thread-affine resources can impose rules beyond ordinary Khora values. See FFI for pointer and suspension constraints, Concurrency for fiber lifetime rules, and Sharing for cross-fiber values.

Khora 0.1.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource