Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

lib/scope.x

Memory allocation scope management.

Primary API

FunctionSummary
x2c_scope_thread_releaseDestroys this thread’s Scope chain and its push and retain stacks.
Scope.newCreates a detached, unnamed scope and returns it.
Scope.popRestores the slot that was active before the matching Scope.push.
Scope.pushMakes the scope in scope active until a matching Scope.pop.

Functions

x2c_scope_thread_release

void x2c_scope_thread_release(void)

Destroys this thread’s Scope chain and its push and retain stacks. It runs last in x2c_thread_state_release and repeats harmlessly; a thread that never created a Scope has nothing to destroy.

Source: lib/scope.x:336

Scope

Scope.new

Scope Scope.new(void)

Creates a detached, unnamed scope and returns it. A detached scope sits in no slot and is not active, so nothing is charged to it until you allocate through Scope.malloc_in and friends or make it active with Scope.push. End it with Scope.destroy. Prefer Scope.new_named for anything long-lived; the name appears in the exit-time leak report.

Raises: <alloc-fail> when the scope cannot be allocated. Before Error initialization it terminates at the error floor.

Source: lib/scope.x:399

Scope.pop

void Scope.pop(void)

Restores the slot that was active before the matching Scope.push. Popping frees nothing. The popped slot keeps its scope and every allocation in it, so you can destroy that scope later or pass it elsewhere. With no pushes outstanding, the process root slot becomes active again.

Raises: <bad-state> when the scope stack is empty, so a mismatched push and pop raises instead of redirecting allocations. The failure leaves the active slot unchanged. Before Error initialization it terminates at the error floor.

Source: lib/scope.x:600

Scope.push

void Scope.push(Scope *scope)

Makes the scope in scope active until a matching Scope.pop. scope is the address of a caller-owned Scope variable, and it may hold NULL: the slot is filled lazily by the first allocation that lands in it. Pushing lets several operations share one lifetime without threading a slot through every call. Slots stack, and each push needs one pop.

int main(void) {
Scope work = NULL;
Scope.push(&work);
char *buffer = Scope.malloc(16);
snprintf(buffer, 16, "in work");
Scope.pop();
puts(buffer);
Scope.destroy(work);
  return 0;
}

The pointer stays valid after the pop. Popping changes which slot is active; work still owns the allocation until it is destroyed. A pushed slot cannot be destroyed while it is on the stack.

Raises: <bad-arg> when scope is NULL, <size-limit> when the stack cannot grow within its representation, or <alloc-fail> when its storage cannot be allocated. These failures leave the active slot unchanged. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:559

Advanced and interop API

FunctionSummary
Scope_shutdownReleases resources owned by Scope.
Scope.callocAllocates count objects of size bytes each, zeroed, in the active scope.
Scope.calloc_inAllocates count zeroed objects of size bytes in the scope in slot.
Scope.destroyDestroys a detached scope and frees every allocation it owns.
Scope.freeFrees one scope-owned allocation before its scope ends.
Scope.mallocAllocates size uninitialized bytes in the active scope.
Scope.malloc_inAllocates size uninitialized bytes in the scope held by slot.
Scope.memdupCopies size bytes from ptr into a new allocation in the active scope.
Scope.memdup_inCopies size bytes from ptr into the scope held by slot.
Scope.moveRelinks one allocation onto the scope held by slot.
Scope.nameReturns the diagnostic name of scope, or NULL if it has none.
Scope.new_namedCreates a detached scope carrying a copy of name for diagnostics.
Scope.reallocResizes one scope-owned allocation and returns the new pointer.
Scope.releaseDestroys the scope in the active slot and frees everything it owns.
Scope.retainOpens a new scope in the active slot and makes it the current one.
Scope.statsReturns a snapshot of the allocator’s counters.
Scope.topReturns the address of the active scope slot.

Functions

Scope_shutdown

void Scope_shutdown(void)

Releases resources owned by Scope. Scope groups managed allocations by lifetime; balanced retain/release and push/pop boundaries remain caller responsibilities.

Source: lib/scope.x:904

Scope

Scope.calloc

void *Scope.calloc(size_t count, size_t size)

Allocates count objects of size bytes each, zeroed, in the active scope. The product is checked for overflow before anything is allocated, and the bytes are set to zero; in every other respect this behaves like Scope.malloc. A request that multiplies out to zero still returns a distinct pointer the scope owns, so it is not a failure signal.

Raises: <size-limit> when the object count overflows, or <alloc-fail> when allocation fails. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:735

Scope.calloc_in

void *Scope.calloc_in(Scope *slot, size_t count, size_t size)

Allocates count zeroed objects of size bytes in the scope in slot. The slot-targeted form of Scope.calloc, with the same overflow check and the same lazy scope creation as Scope.malloc_in; the active scope is left alone.

Raises: <bad-arg> when slot is NULL, <size-limit> when the object count overflows, or <alloc-fail> when allocation fails. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:748

Scope.destroy

void Scope.destroy(Scope scope)

Destroys a detached scope and frees every allocation it owns. This ends a scope you hold in a variable, and is the counterpart to Scope.new and Scope.new_named. It frees the scope’s allocations and discards its name; pointers into it are dangling afterwards, and nothing diagnoses their use. Any regions still linked below it, from retains that were never released, are destroyed with it.

Scope.destroy refuses a scope that is still in use, so a double destroy or a mismatched push and pop raises instead of corrupting the allocation lists. The three refused cases are the active root scope, a slot still on the pushed stack (pop it first), and a scope that a later Scope.retain layered another region on top of, which the runtime reports as an attached lower scope.

Raises: <bad-state> for the active root, a pushed slot, or an attached lower scope. The failure leaves the scope intact. A NULL scope does nothing. Before Error initialization it terminates at the error floor.

Source: lib/scope.x:496

Scope.free

void Scope.free(void *ptr)

Frees one scope-owned allocation before its scope ends. ptr must be a pointer returned by Scope.malloc, Scope.calloc, Scope.memdup, one of their _in forms, or Scope.realloc. It is unlinked from whichever scope owns it now, which after a Scope.move may not be the active one.

Shortening a lifetime this way is normal. Freeing anything else, such as a stack address, an interned String, or a plain malloc result, is undefined, as is freeing the same pointer twice. Nothing diagnoses either.

A NULL ptr does nothing.

Source: lib/scope.x:797

Scope.malloc

void *Scope.malloc(size_t size)

Allocates size uninitialized bytes in the active scope. The result is managed memory. Scope.realloc resizes it, Scope.free ends its life early, Scope.move reassigns its owner, and Scope.release or Scope.destroy reclaims whatever is left. The active scope at the time of allocation owns the result; a later retain or push does not move it.

Raises: <size-limit> when the size would overflow the allocation header, or <alloc-fail> when the underlying allocation fails. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:705

Scope.malloc_in

void *Scope.malloc_in(Scope *slot, size_t size)

Allocates size uninitialized bytes in the scope held by slot. slot is the address of a Scope variable; if it holds NULL, a fresh unnamed scope is created and stored there. Targeting a slot does not touch the active-scope stack, so an intervening Scope.retain or Scope.push cannot redirect the allocation. Pass Scope.top() to name the active slot explicitly.

Raises: <bad-arg> when slot is NULL, <size-limit> when the size overflows, or <alloc-fail> when allocation fails. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:720

Scope.memdup

void *Scope.memdup(const void *ptr, size_t size)

Copies size bytes from ptr into a new allocation in the active scope. The copy is ordinary scope-owned memory, freed by Scope.free or by the release that ends the region. Nothing about the source is remembered, so duplicating a C string means copying its terminator too: Scope.memdup(text, strlen(text) + 1).

A NULL ptr or a zero size returns NULL rather than an empty allocation, so a duplicate of nothing is indistinguishable from failure; check the arguments yourself when that distinction matters.

Raises: <size-limit> or <alloc-fail> from the underlying allocation. A NULL ptr or zero size returns NULL without raising.

Source: lib/scope.x:765

Scope.memdup_in

void *Scope.memdup_in(Scope *slot, const void *ptr, size_t size)

Copies size bytes from ptr into the scope held by slot. The slot-targeted form of Scope.memdup, with the same NULL-for-nothing rule and the same lazy scope creation as Scope.malloc_in. It is the usual way to hand a snapshot of caller data to a scope that outlives the current region.

Raises: <bad-arg> when slot is NULL, or <size-limit> or <alloc-fail> from the underlying allocation. A NULL ptr or zero size returns NULL without raising.

Source: lib/scope.x:779

Scope.move

void Scope.move(void *ptr, Scope *slot)

Relinks one allocation onto the scope held by slot. The bytes are not copied and the pointer does not change; only ownership moves, so a temporary region can compute one result that outlives it. If slot holds NULL a fresh unnamed scope is created there, as the _in allocators do.

int main(void) {
Scope keep = Scope.new_named("results");
Scope.retain();
char *text = Scope.memdup("survivor", 9);
Scope.move(text, &keep);
Scope.release();
puts(text);
Scope.destroy(keep);
  return 0;
}

You do not need this for a String, a List, or a Symbol. Canonical values already outlive the scope that was active when they were built.

Raises: <bad-arg> when slot is NULL, or <alloc-fail> when a new destination scope cannot be allocated. These failures leave ownership unchanged. A NULL ptr does nothing. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:845

Scope.name

const char *Scope.name(Scope scope)

Returns the diagnostic name of scope, or NULL if it has none. The string belongs to the allocator’s name registry and stays valid until the scope is destroyed; do not free it. A scope from Scope.new, and one the runtime created implicitly for the first allocation into an empty slot, both have no name.

Source: lib/scope.x:434

Scope.new_named

Scope Scope.new_named(const char *name)

Creates a detached scope carrying a copy of name for diagnostics. The name is copied, so a temporary buffer is fine. Scope.name reports it, and the allocator prints it at exit if anything the scope owns is still alive, as a line like scope "request": 3 allocations. A NULL name behaves like Scope.new.

int main(void) {
Scope work = Scope.new_named("request");
char *copy = Scope.memdup_in(&work, "payload", 8);
puts(copy);
Scope.destroy(work);
  return 0;
}

Raises: <alloc-fail> when the scope or name copy cannot be allocated, or <size-limit> when the name is too large. Before Error initialization these failures terminate the process.

Source: lib/scope.x:423

Scope.realloc

void *Scope.realloc(void *ptr, size_t size)

Resizes one scope-owned allocation and returns the new pointer. Ownership does not change: the allocation stays with the scope that already held it, even if that is not the active one. Two edge cases follow C’s realloc: a NULL ptr allocates size bytes in the active scope, and a size of zero frees the allocation and returns NULL. A NULL result is not by itself a failure.

As with C, the old pointer must be treated as dead once a resize succeeds.

Raises: <size-limit> when the size overflows, or <alloc-fail> when allocation fails. Before Error initialization these failures terminate at the error floor.

Source: lib/scope.x:876

Scope.release

void Scope.release(void)

Destroys the scope in the active slot and frees everything it owns. Every allocation charged to that scope is freed: Scope.malloc, Scope.calloc, and Scope.memdup results, and the backing storage of the Blocks, Arrays, Buffers, and boxed wide Vars built while it was active. The scope linked below it then becomes active again.

Canonical values are not reclaimed. Interned Strings, List cells, and Symbols built inside the region remain valid after release. An allocation relinked with Scope.move now belongs to its new scope and survives. Anything owned by another scope or another slot is untouched, and memory from plain malloc is unaffected.

The active region must be the one opened by a matching Scope.retain on this same slot. Releasing an empty slot, a directly allocated root, or a region retained on another pushed slot is an imbalance and raises. An extra release cannot destroy the surrounding region, and a retained empty scope still closes normally.

A pointer into a released region is not diagnosed. Neither the compiler nor the runtime tracks it, and using it afterwards is undefined behavior. Scope.stats confirms that a routine leaves nothing live.

Raises: <bad-state> when no matching retain owns the active region. The failure leaves the active region intact. Before Error initialization it terminates at the error floor.

Source: lib/scope.x:680

Scope.retain

void Scope.retain(void)

Opens a new scope in the active slot and makes it the current one. Allocations that follow are charged to the new scope. The scope that was there is linked below it and becomes current again on release, so retained regions nest. Open a nested region when a group of temporaries should die before the surrounding work does. Each Scope.retain must be paired with exactly one Scope.release.

Pair them with defer. Error transfer does not release a scope. A raise that jumps past a plain Scope.release leaves that region live for the rest of the process, so use defer in any function that both retains and can raise.

static void report(int width) {
Scope.retain();
defer Scope.release();
char *line = Scope.malloc(width + 1);
snprintf(line, width + 1, "%d bytes", width);
puts(line);
}

int main(void) {
  report(16);
  return 0;
}

A scope wrapped around nothing but canonical values does nothing, since those values already outlive it, and adds a release that is easy to omit.

Raises: <alloc-fail> when the scope or its retain record cannot be allocated, or <size-limit> when that record cannot grow. Before Error initialization they terminate at the error floor.

Source: lib/scope.x:642

Scope.stats

ScopeStats Scope.stats(void)

Returns a snapshot of the allocator’s counters. live_allocations and live_scopes are derived from the call counts, so read them before and after a routine to check that it leaves nothing behind. The snapshot also carries allocation_calls, reallocation_calls, free_calls, scope_creations, scope_destructions, requested_bytes, and largest_request. Stats remain valid after shutdown.

int main(void) {
size_t before = Scope.stats().live_allocations;
Scope.retain();
char *scratch = Scope.malloc(128);
scratch[0] = 0;
Scope.release();
printf("reclaimed = %d\n", Scope.stats().live_allocations == before);
  return 0;
}

Source: lib/scope.x:460

Scope.top

Scope *Scope.top(void)

Returns the address of the active scope slot. The result is the slot, not the scope. Pass it to the _in allocators to name the active slot explicitly. Dereferencing it gives the scope, which may be NULL before anything has been allocated into the slot. It names the slot that was active at the call; a later Scope.push, Scope.pop, Scope.retain, or Scope.release changes which slot is active, so read it again instead of caching it.

Source: lib/scope.x:585

Runtime-internal callables

These callables connect runtime translation units. They are documented for source readers but are not supported as user API.

FunctionSummary
Scope.initializeInitializes the process-wide Scope runtime owner.
Scope.ownerReturns the Scope that currently owns ptr.
Scope.shutdown_hookRegisters hook to run during process-wide Scope shutdown.

Scope

Scope.initialize

void Scope.initialize(void)

Initializes the process-wide Scope runtime owner.

Source: lib/scope.x:386

Scope.owner

Scope Scope.owner(void *ptr)

Returns the Scope that currently owns ptr. ptr must be a live pointer returned by a Scope allocator. Context uses this to leave ancestor-owned objects where they are while moving results out of its own Scope chain. Passing any other nonnull pointer is undefined behavior, matching Scope.free and Scope.move.

Source: lib/scope.x:811

Scope.shutdown_hook

void Scope.shutdown_hook(void (*hook)(void))

Registers hook to run during process-wide Scope shutdown. The function pointer is retained without being invoked. Shutdown invokes registrations once in reverse order while Scope storage is still available.

Raises: <bad-arg> for a null hook, <size-limit> when the registry cannot grow, or <alloc-fail> when its storage cannot be allocated.

Source: lib/scope.x:516

Public types

TypeKindSummary
ScopestructHandle for a region and any retained regions linked below it.
ScopeAllocstructIntrusive header stored immediately before each Scope-owned allocation.
ScopeStatsstructValue snapshot of process-wide Scope allocation and lifetime counters.

Scope

typedef struct Scope { ScopeAlloc first; struct Scope *down, *up; } *Scope

Handle for a region and any retained regions linked below it. The layout remains public for compatibility, but only Scope operations create or mutate valid allocation and region links.

Source: lib/scope.x:38

ScopeAlloc

typedef struct ScopeAlloc { struct ScopeAlloc *prev, *next; } *ScopeAlloc

Intrusive header stored immediately before each Scope-owned allocation. Its links are allocator-managed; callers must not construct or mutate it.

Source: lib/scope.x:30

ScopeStats

typedef struct ScopeStats { size_t allocation_calls, reallocation_calls, free_calls, live_allocations; size_t scope_creations, scope_destructions, live_scopes, requested_bytes; size_t largest_request; } ScopeStats

Value snapshot of process-wide Scope allocation and lifetime counters. It owns no storage; live counts are derived when the snapshot is taken.

Source: lib/scope.x:46

Design notes

Scope owns groups of individually managed allocations. Callers may use the active scope, target an explicit scope slot, or retain and release a nested lifetime. Explicit free and realloc remain valid for allocations returned by Scope.malloc, Scope.calloc, and Scope.memdup, and Scope.move relinks one allocation onto another scope without copying it.

Scope.initialize owns runtime initialization, while public operations also initialize safely when called before the runtime aggregator. Shutdown hooks run in reverse registration order, after which the module enters a terminal state. Scope.stats remains available after shutdown so callers and tests can inspect the final state.

Tests and examples

make verify (unittest/test-scope.x) and make examples (docs-tour).