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:
- 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, and a region is released when the last reference to it goes –
which for the ordinary case is the end of the block its binding is in, on every
way out of that block, including a cancellation. The exception is a reference
that outlives the block: a closure that captured the region, or a Scope
handed to a fiber, keeps it open until that reference goes too. That is what
lets a fiber hold a scope belonging to the call that spawned it, and it means a
captured region’s finalizers run later than the block, not at it. So the example above
runs its finalizers when work returns, and putting the let inside a smaller
block ends the region there instead:
{ let region = Region::open(); Region::defer(region, fn () => release()); use_it();};// `release()` has already run here.That is the difference between a lease that ends with the call and one that
ends with the caller, and it is worth being deliberate about: a Region::open()
at the top of a function holds everything until the function returns.
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”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.3.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource