x-engine-c v0.2.13
The C engine for x-lang
Loading...
Searching...
No Matches
heap.c File Reference

GC primitives – mark/sweep phases, hooks, roots, alloc limit. More...

#include "x-prim.h"
#include "x-eval.h"
#include "x-heap.h"
#include "x-type.h"
#include "x-type/int.h"
#include "x-type/str.h"
#include "x-obj/prim.h"

Functions

static void x_heap_run_hooks (x_obj_t *p_base, x_obj_t *p_hooks)
 
static void x_heap_mark_phase (x_obj_t *p_base)
 
static void x_heap_sweep_phase (x_obj_t *p_base)
 
static x_obj_tx_prim_heap_sweep (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_count (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_alloc_limit (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_mark (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_collect (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_system_mark (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_mark_hook (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_free_hook (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_mark_root (x_obj_t *p_base, x_obj_t *p_args)
 
static x_obj_tx_prim_heap_tree_mark (x_obj_t *p_base, x_obj_t *p_args)
 Mark a tree with caller-chosen flags. x-lang: (heap tree-mark! obj flags)
 
static x_obj_tx_prim_heap_chain_clear (x_obj_t *p_base, x_obj_t *p_args)
 Clear flags across the allocation chain. x-lang: (heap chain-clear! flags)
 
x_obj_tx_prim_heap_register (x_obj_t *p_base, x_obj_t *p_args)
 

Detailed Description

GC primitives – mark/sweep phases, hooks, roots, alloc limit.

Author
Jon Ruttan (jonru.nosp@m.ttan.nosp@m.@gmai.nosp@m.l.co.nosp@m.m)

Function Documentation

◆ x_heap_mark_phase()

static void x_heap_mark_phase ( x_obj_t p_base)
static

Mark phase: trace live objects from every root (GC phase 1).

Four passes: (1) fire mark hooks, (2) tree-mark from the base data tree, (3) root-chain walk – the off-chain stack objects frames registered via x_heap_root_push, (4) tree-mark each registered GC root. The hook/root lists live in x-expr's heap-group; x-expr stores but cannot dispatch (no callable mechanism at that layer), so the walk + invoke happens here.

Hooks MUST fire before the marking passes: everything a hook allocates is born unmarked, and the sweep that follows this phase frees every unmarked object. Firing hooks first means a hook allocation that escaped into reachable state – a (heap-mark-root!) spine cell, a set! into a global – is marked by the later passes and survives, while the hook's transient garbage is correctly swept. It also makes a root registered mid-collect count in the same cycle (pass 4 runs after the hooks push it). With hooks last, the sweep freed the escaped cells and left reachable dangling pointers – a use-after-free on the next collect (usually silent: the chunk is recycled and the mark walk traverses a reinterpreted object; ASan catches it only when the chunk stays unreused).

Note
A sweep must run with no allocation between it and this mark: a transient cell allocated after the mark is unmarked, so an intervening sweep frees it while the evaluator is still traversing it (see x_prim_heap_collect).
See also
x_heap_sweep_phase

◆ x_heap_run_hooks()

static void x_heap_run_hooks ( x_obj_t p_base,
x_obj_t p_hooks 
)
static

Fire each hook on a hook list, draining any deferred tail call. Shared by the mark and free hook walks (mark_phase / sweep_phase). A procedure hook that returns a non-nil tail leaves the env extended and tco_expr/ tco_env set for an outer trampoline; there is none here, so drain it via x_eval_tco_trampoline – otherwise the env frame the hook left live is freed by the sweep and the next eval dereferences it.

◆ x_heap_sweep_phase()

static void x_heap_sweep_phase ( x_obj_t p_base)
static

Sweep phase: fire free hooks, then reclaim unmarked objects (GC phase 2). x_heap_sweep also clears the mark flag on retained objects, readying them for the next cycle.

See also
x_heap_mark_phase

◆ x_prim_alloc_limit()

static x_obj_t * x_prim_alloc_limit ( x_obj_t p_base,
x_obj_t p_args 
)
static

Set the allocation ceiling (the runaway-memory guard). x-lang: (alloc-limit! n) – n > 0 arms the guard; 0 disables (the default; negative input is treated as 0).

When armed, x_obj_alloc reports an error through the standard error path and stops the process rather than allocate past n objects; once tripped the limit latches, so an intercepting guard handler cannot spin it (see x_obj_alloc). The trip-message text is stored here at arm time – x-expr's mechanism layer holds no prose. Configuration is in-language (the interpreter reads no environment): the spec runner feeds (alloc-limit! n) ahead of each library load, so a runaway ./x stops itself instead of exhausting system memory. A development guard against runaway allocation, not a sandbox – code can re-set or disable it.

Parameters
p_baseBase (execution context).
p_argsUnevaluated (n); x_eargs evaluates it.
Returns
NULL.
Note
Fexpr: args unevaluated; x_eargs evaluates n.

◆ x_prim_heap_chain_clear()

static x_obj_t * x_prim_heap_chain_clear ( x_obj_t p_base,
x_obj_t p_args 
)
static

Clear flags across the allocation chain. x-lang: (heap chain-clear! flags)

The counterpart to the above: sweeping would clear the flag too, but sweeping also frees. A chain clear reaches every object, including whatever became garbage since the mark, which a tree walk would leave flagged.

Parameters
p_baseBase (execution context).
p_argsUnevaluated: (self flags).
Returns
nil.

◆ x_prim_heap_collect()

static x_obj_t * x_prim_heap_collect ( x_obj_t p_base,
x_obj_t p_args 
)
static

Run a full, atomic garbage collection cycle (mark then sweep). x-lang: (heap-collect)

This is the safe GC entry point. Mark and sweep run back-to-back in one C call with no x-lang-level evaluation – and therefore no allocation – between them. That matters because eval-list scratch cells (and the env/ctrl/extras half of the base tree) are allocated X_OBJ_FLAG_NONE and survive a sweep only by being marked. The mark phase here runs while the (heap-collect) call's own eval-list frame is live, so that frame is marked and survives the immediately following sweep. Splitting mark and sweep into two separate evaluations (e.g. (begin (heap-mark) (heap-sweep))) reintroduces an allocation between them and frees the in-flight frame – hence (heap-collect), not the raw phases, is the supported API.

Parameters
p_baseBase (execution context).
p_argsUnused.
Returns
NULL.
Note
Increments the GC run counter when X_PROFILE is defined.
See also
x_prim_heap_mark, x_prim_heap_sweep, x_prim_heap_count

◆ x_prim_heap_count()

static x_obj_t * x_prim_heap_count ( x_obj_t p_base,
x_obj_t p_args 
)
static

Count the number of objects currently on the heap. x-lang: (heap-count)

Parameters
p_baseBase (execution context).
p_argsUnused.
Returns
Integer with the heap object count.
See also
x_prim_heap_collect, x_prim_heap_mark, x_prim_heap_sweep

◆ x_prim_heap_free_hook()

static x_obj_t * x_prim_heap_free_hook ( x_obj_t p_base,
x_obj_t p_args 
)
static

Register a callable to run during the GC sweep phase. x-lang: (heap-free-hook! hook)

Parameters
p_baseBase (execution context).
p_argsUnevaluated (hook).
Returns
NULL.
See also
x_heap_free_hook_add

◆ x_prim_heap_mark()

static x_obj_t * x_prim_heap_mark ( x_obj_t p_base,
x_obj_t p_args 
)
static

Mark all reachable objects on the heap (GC phase 1, low-level). x-lang: (heap-mark)

LOW-LEVEL. Marking alone is harmless (it frees nothing), but a mark is only useful paired with a sweep that runs with no allocation in between. Use (heap-collect) for a safe, atomic mark+sweep.

Parameters
p_baseBase (execution context).
p_argsUnused.
Returns
NULL.
See also
x_prim_heap_collect, x_prim_heap_sweep, x_prim_heap_count

◆ x_prim_heap_mark_hook()

static x_obj_t * x_prim_heap_mark_hook ( x_obj_t p_base,
x_obj_t p_args 
)
static

Register a callable to run during the GC mark phase. x-lang: (heap-mark-hook! hook)

Parameters
p_baseBase (execution context).
p_argsUnevaluated (hook).
Returns
NULL.
Note
Storage in x-expr's heap-group (one canonical location).
See also
x_heap_mark_hook_add

◆ x_prim_heap_mark_root()

static x_obj_t * x_prim_heap_mark_root ( x_obj_t p_base,
x_obj_t p_args 
)
static

Register an object to mark on every collection (extra GC root). x-lang: (heap-mark-root! obj)

Parameters
p_baseBase (execution context).
p_argsUnevaluated (obj).
Returns
NULL.
See also
x_heap_mark_root_add

◆ x_prim_heap_register()

x_obj_t * x_prim_heap_register ( x_obj_t p_base,
x_obj_t p_args 
)

Register the GC primitives.

◆ x_prim_heap_sweep()

static x_obj_t * x_prim_heap_sweep ( x_obj_t p_base,
x_obj_t p_args 
)
static

Sweep unmarked objects from the heap (GC phase 2, low-level). x-lang: (heap-sweep)

LOW-LEVEL / UNSAFE on its own. A sweep frees every object not marked by a preceding mark, so calling (heap-sweep) without an immediately preceding (heap-mark) – or with any allocation in between – frees live data, including the eval-list cell the evaluator is mid-traversal on. Use (heap-collect) for a safe, atomic mark+sweep. This primitive is exposed only for instrumentation that controls the phases manually with no intervening allocation.

Parameters
p_baseBase (execution context).
p_argsUnused.
Returns
NULL.
See also
x_prim_heap_collect, x_prim_heap_mark, x_prim_heap_count

◆ x_prim_heap_tree_mark()

static x_obj_t * x_prim_heap_tree_mark ( x_obj_t p_base,
x_obj_t p_args 
)
static

Mark a tree with caller-chosen flags. x-lang: (heap tree-mark! obj flags)

x_heap_tree_mark is the collector's own traversal, and it takes the flag it sets as a parameter. Exposed, it answers a question no walk written in x-lang can: what is reachable from here, counting the base sentinel, the custom mark handlers, the mark hooks and the root chain.

WHICH FLAG IS THE CALLER'S PROBLEM, and the caller had better not pick one the collector owns: the flag doubles as the traversal's visited test, so X_OBJ_FLAG_SHARED halts at the first base-tree node, and a leftover X_OBJ_FLAG_MARK makes the next mark phase stop short and its sweep free the children it missed. The layout descriptor names a bit reserved for this.

Parameters
p_baseBase (execution context).
p_argsUnevaluated: (self obj flags).
Returns
The object marked from.

◆ x_prim_system_mark()

static x_obj_t * x_prim_system_mark ( x_obj_t p_base,
x_obj_t p_args 
)
static

Recursively mark an object and all reachable objects as SYSTEM (GC-immune). x-lang: (gc-pin! obj)

Parameters
p_baseBase (execution context).
p_argsUnevaluated argument list (obj).
Returns
The marked object.
Note
Fexpr: args unevaluated; x_eargs evaluates obj.
Uses X_OBJ_FLAG_SHARED to make objects immune to GC sweep.
See also
x_prim_heap_mark, x_prim_heap_sweep