Memory and resources
Khora manages ordinary memory automatically. Resource lifetimes that have external meaning—files, sockets, transactions, foreign handles—use explicit structured cleanup.
Ordinary memory
Section titled “Ordinary memory”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.
Region syntax
Section titled “Region syntax”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.
Scope capability
Section titled “Scope capability”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 'erExample:
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.
Acquire and release
Section titled “Acquire and release”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.
Exit semantics
Section titled “Exit semantics”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.
Resource APIs
Section titled “Resource APIs”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 'errather 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