lib/scope.x
Memory allocation scope management.
Primary API
| Function | Summary |
|---|---|
x2c_scope_thread_release | Destroys this thread’s Scope chain and its push and retain stacks. |
Scope.new | Creates a detached, unnamed scope and returns it. |
Scope.pop | Restores the slot that was active before the matching Scope.push. |
Scope.push | Makes 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
| Function | Summary |
|---|---|
Scope_shutdown | Releases resources owned by Scope. |
Scope.calloc | Allocates count objects of size bytes each, zeroed, in the active scope. |
Scope.calloc_in | Allocates count zeroed objects of size bytes in the scope in slot. |
Scope.destroy | Destroys a detached scope and frees every allocation it owns. |
Scope.free | Frees one scope-owned allocation before its scope ends. |
Scope.malloc | Allocates size uninitialized bytes in the active scope. |
Scope.malloc_in | Allocates size uninitialized bytes in the scope held by slot. |
Scope.memdup | Copies size bytes from ptr into a new allocation in the active scope. |
Scope.memdup_in | Copies size bytes from ptr into the scope held by slot. |
Scope.move | Relinks one allocation onto the scope held by slot. |
Scope.name | Returns the diagnostic name of scope, or NULL if it has none. |
Scope.new_named | Creates a detached scope carrying a copy of name for diagnostics. |
Scope.realloc | Resizes one scope-owned allocation and returns the new pointer. |
Scope.release | Destroys the scope in the active slot and frees everything it owns. |
Scope.retain | Opens a new scope in the active slot and makes it the current one. |
Scope.stats | Returns a snapshot of the allocator’s counters. |
Scope.top | Returns 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.
| Function | Summary |
|---|---|
Scope.initialize | Initializes the process-wide Scope runtime owner. |
Scope.owner | Returns the Scope that currently owns ptr. |
Scope.shutdown_hook | Registers 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
| Type | Kind | Summary |
|---|---|---|
Scope | struct | Handle for a region and any retained regions linked below it. |
ScopeAlloc | struct | Intrusive header stored immediately before each Scope-owned allocation. |
ScopeStats | struct | Value 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).