///|
/// Central coordinator for the incremental computation framework.
///
/// The Runtime manages all bookkeeping for dependency tracking, revision
/// counting, and batch operations. Every Input and Derived is associated
/// with exactly one Runtime.
///
/// # Pointer chase tradeoff
///
/// All sub-struct field accesses (e.g. `self.core.revision.current_revision`)
/// require one extra pointer dereference compared to the old flat layout.
/// MoonBit structs are heap-allocated, so RuntimeCore is a separate heap
/// object. In hot loops (verify dep-walk, push propagation), the sub-struct
/// pointers are cache-hot since they are dereferenced on every iteration.
///
/// # Example
///
/// ```moonbit nocheck
/// let rt = Runtime()
///
/// let x = Input(rt, 10)
///
/// let doubled = Memo(rt, () => x.get() * 2)
/// ```
pub(all) struct Runtime {
priv core : RuntimeCore
priv pull : PullState
priv push : PushState
priv datalog : DatalogState
priv mut evaluation_strategies : EvaluationStrategies
// cell_lifecycle is lifted out of RuntimeCore: the CellLifecycle trait
// takes `Runtime` as a parameter (see cells/cell_ops.mbt), so moving it
// into kernel would create a circular type reference.
priv cell_lifecycle : Array[&CellLifecycle]
// Commit-phase hooks for pull-mode memo recomputes, dispatched from
// `memo_force_recompute`. Same placement rationale as cell_lifecycle:
// MemoCommitPhase takes Runtime as a parameter (see
// cells/memo_commit_phase.mbt), so the array cannot live in kernel.
priv commit_hooks : Array[&MemoCommitPhase]
priv mut static_recompute_depth : Int
priv static_recompute_tracking_floors : Array[Int]
priv static_recompute_previous_runtime_ids : Array[@incr_types.RuntimeId?]
priv accumulator_commit_hook : AccumulatorCommitHook
priv event_broadcast_hook : EventBroadcastPhaseHook
priv event_broadcast_hook_index : Int
priv runtime_evaluation_event_hook : RuntimeEvaluationEventHook
priv runtime_evaluation_event_hook_index : Int
// Accumulator state. Monotonic array; disposed slots keep their index
// (their `disposed` field is set true). No slot-id reuse.
priv accumulator_slots : Array[SlotMeta]
// Cached trait-object view of `accumulator_slots`, kept index-aligned
// by `register_accumulator_slot`. Runtime returns this array directly
// from `slot_snapshots()` so kernel-side verify code does not allocate
// per call.
priv accumulator_snapshots : Array[&@shared.SlotSnapshot]
priv mut next_accumulator_id : Int
// Reverse index: for each memo, which accumulator slots it has pushed to.
// Populated on successful recompute commit; used for BEFORE_CLOSURE snapshot
// iteration and memo disposal cleanup.
priv accumulator_contributions : @hashmap.HashMap[
@incr_types.CellId,
@hashset.HashSet[@incr_types.AccumulatorId],
]
}
///|
/// Deprecated alias of the `Runtime::Runtime` constructor.
#deprecated("Use the constructor form `Runtime()` (`Runtime::Runtime`) instead.")
pub fn Runtime::new(on_change? : () -> Unit) -> Runtime {
Runtime(on_change?)
}
///|
/// Returns this runtime's identity.
///
/// Use it to ask "are these two runtimes the same?" (`a.id() == b.id()`)
/// without allocating a probe cell to read a `CellId`'s `runtime_id`. A
/// `RuntimeId` is a debug / introspection identity, not a stable application
/// key — see `RuntimeId`.
pub fn Runtime::id(self : Runtime) -> @incr_types.RuntimeId {
self.core.runtime_id
}
///|
/// Creates a new runtime with an empty dependency graph. Enables `Runtime()`
/// / `Runtime(on_change=...)` call sites.
///
/// # Parameters
///
/// - `on_change`: Optional callback invoked whenever any input changes
/// (or at the end of a batch if values actually changed).
pub fn Runtime::Runtime(on_change? : () -> Unit) -> Runtime {
let accumulator_hook = AccumulatorCommitHook::new()
let event_hook = EventBroadcastPhaseHook::new()
let runtime_event_hook = RuntimeEvaluationEventHook::new()
let rt : Runtime = {
core: @kernel.RuntimeCore(),
pull: { inputs: [], memos: [], free_inputs: [], free_memos: [] },
push: {
reactives: [],
effects: [],
free_reactives: [],
free_effects: [],
node_count: 0,
},
datalog: { relations: [], functional_relations: [], rules: [] },
evaluation_strategies: EvaluationStrategies::default(),
cell_lifecycle: [],
commit_hooks: [],
static_recompute_depth: 0,
static_recompute_tracking_floors: [],
static_recompute_previous_runtime_ids: [],
accumulator_commit_hook: accumulator_hook,
event_broadcast_hook: event_hook,
event_broadcast_hook_index: 1,
runtime_evaluation_event_hook: runtime_event_hook,
runtime_evaluation_event_hook_index: 2,
accumulator_slots: [],
accumulator_snapshots: [],
next_accumulator_id: 0,
accumulator_contributions: @hashmap.HashMap([]),
}
// Register the same hook objects in dispatch order.
rt.commit_hooks.push(accumulator_hook)
rt.commit_hooks.push(event_hook)
rt.commit_hooks.push(runtime_event_hook)
// Seed the optional constructor on_change callback through the registry's
// singleton slot, preserving `Runtime(on_change=...)` semantics.
match on_change {
Some(f) => rt.set_on_change(f)
None => ()
}
rt
}
///|
/// Replaces the sealed internal evaluation strategy bundle.
fn Runtime::install_evaluation_strategies(
self : Runtime,
strategies : EvaluationStrategies,
) -> Unit {
self.runtime_evaluation_event_hook.trace_recorder = strategies.trace_recorder()
self.evaluation_strategies = strategies
}
///|
fn Runtime::event_broadcast_enabled(self : Runtime) -> Bool {
!self.event_broadcast_hook.listeners.is_empty()
}
///|
/// Allocates a fresh listener id (shared counter across both registries).
fn Runtime::alloc_listener_id(self : Runtime) -> ListenerId {
@kernel.alloc_listener_id(self.core)
}
///|
fn Runtime::runtime_evaluation_events_enabled(self : Runtime) -> Bool {
self.runtime_evaluation_event_hook.enabled()
}
///|
fn Runtime::has_pending_events(self : Runtime) -> Bool {
!self.event_broadcast_hook.pending.is_empty() ||
!self.runtime_evaluation_event_hook.pending.is_empty()
}
///|
fn Runtime::runtime_evaluation_event_sink(
self : Runtime,
) -> ((@kernel.RuntimeEvaluationEvent) -> Unit)? {
if self.runtime_evaluation_event_hook.listener_enabled() {
Some(fn(event) { self.runtime_evaluation_event_hook.push_event(event) })
} else {
None
}
}
///|
fn Runtime::is_listener_mutation_safe(self : Runtime) -> Bool {
self.core.phase is Idle &&
self.core.callback_depth == 0 &&
self.core.tracking.stack.is_empty() &&
self.core.batch.depth == 0 &&
self.static_recompute_depth == 0 &&
!self.has_pending_events() &&
!self.event_broadcast_hook.draining &&
!self.runtime_evaluation_event_hook.draining
}
///|
fn Runtime::derived_event_listener_busy_message(operation : String) -> String {
operation +
" cannot be called while an operation is in flight: drain in progress, " +
"events buffered, callback active, batch open, recompute active, or non-Idle phase " +
"(PushPropagating/InFixpoint/GarbageCollecting)"
}
///|
/// Registers the singleton derived recompute lifecycle event listener.
///
/// Re-registering replaces the previous singleton listener in place (its
/// registration position is preserved). Coexists with any additive listeners
/// registered via `add_derived_event_listener`. Raises `Failure` unless the
/// runtime is between operations (see `is_listener_mutation_safe`).
pub fn Runtime::on_derived_event(
self : Runtime,
f : (DerivedEvent) -> Unit,
) -> Unit raise Failure {
guard self.is_listener_mutation_safe() else {
fail(
Runtime::derived_event_listener_busy_message("Runtime::on_derived_event"),
)
}
self.event_broadcast_hook.listeners.set_singleton(
() => self.alloc_listener_id(),
f,
)
}
///|
/// Clears the singleton derived recompute lifecycle event listener. Idempotent;
/// additive listeners are unaffected. Raises `Failure` unless the runtime is
/// between operations.
pub fn Runtime::clear_derived_event_listener(
self : Runtime,
) -> Unit raise Failure {
guard self.is_listener_mutation_safe() else {
fail(
Runtime::derived_event_listener_busy_message(
"Runtime::clear_derived_event_listener",
),
)
}
self.event_broadcast_hook.listeners.clear_singleton()
}
///|
/// Adds a composable derived-event listener and returns its `ListenerId`.
///
/// Multiple additive listeners (and the singleton) coexist on one runtime and
/// fire event-major in registration order. Remove a specific listener with
/// `remove_derived_event_listener`. Raises `Failure` unless the runtime is
/// between operations (same idle guard as `on_derived_event`): the derived-event
/// hook buffers events, so registration must not race a buffered/draining
/// window.
pub fn Runtime::add_derived_event_listener(
self : Runtime,
f : (DerivedEvent) -> Unit,
) -> ListenerId raise Failure {
guard self.is_listener_mutation_safe() else {
fail(
Runtime::derived_event_listener_busy_message(
"Runtime::add_derived_event_listener",
),
)
}
let id = self.alloc_listener_id()
self.event_broadcast_hook.listeners.add(id, f)
id
}
///|
/// Removes the additive derived-event listener with `id`. Idempotent — an
/// unknown or already-removed id is a no-op. Raises `Failure` unless the runtime
/// is between operations (same idle guard rationale as registration).
pub fn Runtime::remove_derived_event_listener(
self : Runtime,
id : ListenerId,
) -> Unit raise Failure {
guard self.is_listener_mutation_safe() else {
fail(
Runtime::derived_event_listener_busy_message(
"Runtime::remove_derived_event_listener",
),
)
}
self.event_broadcast_hook.listeners.remove(id) |> ignore
}
///|
fn Runtime::dispatch_before_recompute_hooks(
self : Runtime,
cell_id : CellId,
) -> Unit {
let event_enabled = self.event_broadcast_enabled()
let runtime_event_enabled = self.runtime_evaluation_events_enabled()
if !event_enabled &&
!runtime_event_enabled &&
self.commit_hooks.length() == self.runtime_evaluation_event_hook_index + 1 {
MemoCommitPhase::before_recompute(
self.accumulator_commit_hook,
self,
cell_id,
)
return
}
for i = 0; i < self.commit_hooks.length(); i = i + 1 {
if (event_enabled || i != self.event_broadcast_hook_index) &&
(runtime_event_enabled || i != self.runtime_evaluation_event_hook_index) {
self.commit_hooks[i].before_recompute(self, cell_id)
}
}
}
///|
fn Runtime::dispatch_after_abort_hooks(
self : Runtime,
cell_id : CellId,
error : Error,
) -> Unit {
let event_enabled = self.event_broadcast_enabled()
let runtime_event_enabled = self.runtime_evaluation_events_enabled()
if !event_enabled &&
!runtime_event_enabled &&
self.commit_hooks.length() == self.runtime_evaluation_event_hook_index + 1 {
MemoCommitPhase::after_abort(
self.accumulator_commit_hook,
self,
cell_id,
error,
)
return
}
for i = 0; i < self.commit_hooks.length(); i = i + 1 {
if (event_enabled || i != self.event_broadcast_hook_index) &&
(runtime_event_enabled || i != self.runtime_evaluation_event_hook_index) {
self.commit_hooks[i].after_abort(self, cell_id, error)
}
}
}
///|
fn Runtime::dispatch_after_success_hooks(
self : Runtime,
cell_id : CellId,
) -> Unit {
let event_enabled = self.event_broadcast_enabled()
let runtime_event_enabled = self.runtime_evaluation_events_enabled()
if !event_enabled &&
!runtime_event_enabled &&
self.commit_hooks.length() == self.runtime_evaluation_event_hook_index + 1 {
MemoCommitPhase::after_success(self.accumulator_commit_hook, self, cell_id)
return
}
for i = 0; i < self.commit_hooks.length(); i = i + 1 {
if (event_enabled || i != self.event_broadcast_hook_index) &&
(runtime_event_enabled || i != self.runtime_evaluation_event_hook_index) {
self.commit_hooks[i].after_success(self, cell_id)
}
}
}
///|
fn Runtime::drain_pending_events_direct(self : Runtime) -> Unit {
if self.static_recompute_depth != 0 {
return
}
let mut progressed = true
while self.has_pending_events() && progressed {
progressed = false
if !self.event_broadcast_hook.pending.is_empty() &&
!self.event_broadcast_hook.draining {
self.event_broadcast_hook.drain()
progressed = true
}
if !self.runtime_evaluation_event_hook.pending.is_empty() &&
!self.runtime_evaluation_event_hook.draining {
self.runtime_evaluation_event_hook.drain()
progressed = true
}
}
}
///|
/// Drains pending events when no recompute frame or callback is active.
fn Runtime::drain_pending_events_if_idle(self : Runtime) -> Unit {
if self.core.callback_depth != 0 ||
!self.core.tracking.stack.is_empty() ||
self.static_recompute_depth != 0 {
return
}
self.drain_pending_events_direct()
}
///|
/// Transitions to a new propagation phase. Delegates to `@kernel.enter_phase`.
fn Runtime::enter_phase(self : Runtime, next : PropagationPhase) -> Unit {
@kernel.enter_phase(self.core, next)
}
///|
/// Returns to the Idle phase. Delegates to `@kernel.leave_phase`.
fn Runtime::leave_phase(self : Runtime) -> Unit {
@kernel.leave_phase(self.core)
}
///|
/// Allocates a raw cell identifier (internal helper).
///
/// Increments `next_cell_id` and returns the new CellId without touching
/// `cell_index`. Used by `alloc_cell_id(CellRef)`.
fn Runtime::alloc_next_id(self : Runtime) -> CellId {
let id = self.core.next_cell_id
self.core.next_cell_id = id + 1
{ runtime_id: self.core.runtime_id, id }
}
///|
/// Allocates a fresh cell identifier and registers it in the SoA cell_index.
///
/// The `cell_ref` argument records which typed SoA array this cell lives in
/// (e.g. `PullInput(idx)` or `PullMemo(idx)`). The returned `CellId.id`
/// is the index into `cell_index` for fast dispatch.
fn Runtime::alloc_cell_id(self : Runtime, cell_ref : CellRef) -> CellId {
let cell_id = self.alloc_next_id()
self.core.cell_index.push(cell_ref)
cell_id
}
///|
/// Installs a new cell into a reusable-slot SoA array and registers it in
/// the unified dispatch tables (`cell_ops`, `cell_lifecycle`). Shared by
/// Input, Derived, EagerDerived, and Effect constructors.
fn[T : CellOps + CellLifecycle] Runtime::install_cell(
self : Runtime,
free_list : Array[Int],
arr : Array[T],
make_ref : (Int) -> CellRef,
make_data : (CellId) -> T,
) -> (CellId, Int) {
let idx = free_list.pop().unwrap_or(arr.length())
let cell_id = self.alloc_cell_id(make_ref(idx))
let data = make_data(cell_id)
if idx < arr.length() {
arr[idx] = data
} else {
arr.push(data)
}
self.core.cell_ops.push(arr[idx])
self.cell_lifecycle.push(arr[idx])
(cell_id, idx)
}
///|
/// Debug invariant: the three unified dispatch tables (`cell_index`,
/// `cell_ops`, `cell_lifecycle`) must stay index-aligned.
fn Runtime::check_table_invariant(self : Runtime) -> Unit {
let ci = self.core.cell_index.length()
let co = self.core.cell_ops.length()
let cl = self.cell_lifecycle.length()
if ci != co || co != cl {
abort(
"Runtime dispatch tables out of sync: cell_index=" +
ci.to_string() +
", cell_ops=" +
co.to_string() +
", cell_lifecycle=" +
cl.to_string(),
)
}
}
///|
/// Debug invariant: `accumulator_slots` and `accumulator_snapshots` must
/// stay index-aligned. Only `register_accumulator_slot` writes both; this
/// assertion exists to catch a future mutation path that forgets to
/// update the cached trait-object view (Stage 3+ may add one).
fn Runtime::check_accumulator_cache_invariant(self : Runtime) -> Unit {
let a = self.accumulator_slots.length()
let b = self.accumulator_snapshots.length()
if a != b {
abort(
"accumulator cache out of sync: slots=" +
a.to_string() +
", snapshots=" +
b.to_string(),
)
}
}
///|
/// Validates that a CellId belongs to this runtime and is within bounds.
/// Aborts with a descriptive message on failure.
fn Runtime::validate_cell(self : Runtime, id : CellId, caller : String) -> Unit {
@kernel.validate_cell(self.core, id, caller)
}
///|
/// Returns true if the CellId belongs to this runtime and is within bounds.
/// Non-aborting variant for methods that return None/[] on invalid input.
fn Runtime::validate_cell_soft(self : Runtime, id : CellId) -> Bool {
@kernel.validate_cell_soft(self.core, id)
}
///|
/// Returns true if a cell has been disposed or its id is out of bounds.
fn Runtime::is_cell_disposed(self : Runtime, cell_id : CellId) -> Bool {
@kernel.is_cell_disposed(self.core, cell_id)
}
///|
/// Returns the `changed_at` revision for any cell, dispatching via cell_index.
fn Runtime::get_changed_at(self : Runtime, id : CellId) -> Revision {
@kernel.get_changed_at(self.core, id)
}
///|
/// Returns the `durability` for any cell, dispatching via cell_index.
fn Runtime::get_durability(self : Runtime, id : CellId) -> Durability {
@kernel.get_durability(self.core, id)
}
///|
/// Returns the PullInputData for an input cell.
fn Runtime::get_pull_input(self : Runtime, id : CellId) -> PullInputData {
self.validate_cell(id, "get_pull_input")
match self.core.cell_index[id.id] {
PullInput(idx) => self.pull.inputs[idx]
_ =>
abort(
"Expected input cell but found different kind: " + id.id.to_string(),
)
}
}
///|
/// Returns the MemoData for a memo or hybrid memo cell.
fn Runtime::get_memo_data(self : Runtime, id : CellId) -> MemoData {
self.validate_cell(id, "get_memo_data")
match self.core.cell_index[id.id] {
PullMemo(idx) | HybridMemo(idx) => self.pull.memos[idx]
_ =>
abort("Expected memo cell but found different kind: " + id.id.to_string())
}
}
///|
/// Returns an iterator over the subscriber IDs for the given cell.
fn Runtime::get_subscribers(self : Runtime, cell_id : CellId) -> Iter[CellId] {
@kernel.get_subscribers(self.core, cell_id)
}
///|
/// Returns the cached trait-object view of accumulator slots.
///
/// Stage 2 adds this accessor so forthcoming kernel algorithms can reach
/// slot state through the `SlotSnapshot` trait without depending on the
/// concrete (private) `SlotMeta` struct. The array is kept index-aligned
/// with `accumulator_slots` by `register_accumulator_slot`.
///
/// The returned array aliases the runtime's cached field — callers must
/// treat it as read-only. Pushing, removing, or replacing entries would
/// break the `accumulator_slots` ↔ `accumulator_snapshots` alignment
/// asserted by `check_accumulator_cache_invariant`.
fn Runtime::slot_snapshots(self : Runtime) -> Array[&@shared.SlotSnapshot] {
self.accumulator_snapshots
}
///|
/// Removes `subscriber` from the subscriber set of `dep` and decrements the
/// subscriber's push contribution from upstream cells.
fn Runtime::remove_subscriber(
self : Runtime,
dep : CellId,
subscriber : CellId,
) -> Unit {
@kernel.remove_subscriber(
self.core,
self.pull,
self.push,
self.datalog,
dep,
subscriber,
)
}
///|
/// Adds `subscriber` to the subscriber set of `dep` and propagates the
/// subscriber's push contribution upstream through the pull dep graph.
fn Runtime::add_subscriber(
self : Runtime,
dep : CellId,
subscriber : CellId,
) -> Unit {
@kernel.add_subscriber(
self.core,
self.pull,
self.push,
self.datalog,
dep,
subscriber,
)
}
///|
/// Registers the singleton on-change callback that fires whenever a revision
/// bump occurs.
///
/// Re-registering replaces the previous singleton in place (registration
/// position preserved). Coexists with additive listeners registered via
/// `add_on_change_listener`. Unlike the derived-event hook, on-change has no
/// buffer/drain state and is read as a snapshot at one well-defined point, so
/// registration is not phase-guarded.
pub fn Runtime::set_on_change(self : Runtime, f : () -> Unit) -> Unit {
self.core.on_change_listeners.set_singleton(() => self.alloc_listener_id(), f)
}
///|
/// Removes the singleton on-change callback. Idempotent; additive listeners are
/// unaffected.
pub fn Runtime::clear_on_change(self : Runtime) -> Unit {
self.core.on_change_listeners.clear_singleton()
}
///|
/// Adds a composable on-change listener and returns its `ListenerId`.
///
/// Multiple additive listeners (and the singleton) coexist and fire in
/// registration order on every revision bump. Remove a specific listener with
/// `remove_on_change_listener`. Not phase-guarded (see `set_on_change`).
pub fn Runtime::add_on_change_listener(
self : Runtime,
f : () -> Unit,
) -> ListenerId {
let id = self.alloc_listener_id()
self.core.on_change_listeners.add(id, f)
id
}
///|
/// Removes the additive on-change listener with `id`. Idempotent — an unknown or
/// already-removed id is a no-op.
pub fn Runtime::remove_on_change_listener(
self : Runtime,
id : ListenerId,
) -> Unit {
self.core.on_change_listeners.remove(id) |> ignore
}
///|
/// Fires the on_change callback if one is registered.
fn Runtime::fire_on_change(self : Runtime) -> Unit {
@kernel.fire_on_change(self.core)
}
///|
/// Core propagation coordinator: advances the revision, stamps changed_at on
/// all changed cells, and runs push propagation if any push cells exist.
fn Runtime::propagate_changes(
self : Runtime,
changed_ids : Array[CellId],
durability : Durability,
) -> Unit {
self.evaluation_strategies.propagate_changes(
self.core,
self.pull,
self.push,
self.datalog,
changed_ids,
durability,
self.runtime_evaluation_event_sink(),
)
}
///|
/// Kernel notification protocol: notifies the runtime that a set of cells
/// has new values.
fn Runtime::publish_cell_changes(
self : Runtime,
changed_ids : Array[CellId],
durability : Durability,
) -> Unit {
self.evaluation_strategies.publish_cell_changes(
self.core,
self.pull,
self.push,
self.datalog,
changed_ids,
durability,
self.runtime_evaluation_event_sink(),
)
}
///|
/// Advance the global revision counter and record which durability level changed.
impl RevisionManager for Runtime with fn advance_revision(self, durability) {
@kernel.advance_revision(self.core, durability)
}
///|
/// Bumps the global revision counter and records which durability level changed.
impl RevisionManager for Runtime with fn bump_revision(self, durability) {
if self.core.batch.depth > 0 {
// Track the maximum durability seen during this batch
if durability > self.core.batch.max_durability {
self.core.batch.max_durability = durability
}
return
}
RevisionManager::advance_revision(self, durability)
}
///|
/// Disposes a rule cell. Public because RuleId has no runtime reference,
/// so the user must call `rt.dispose_rule(rule_id)` directly.
pub fn Runtime::dispose_rule(
self : Runtime,
rule_id : @incr_types.RuleId,
) -> Unit {
self.dispose_cell(rule_id.id)
}
///|
/// Attempts cell disposal through the lifecycle coordinator.
///
/// Lifecycle preflight rejections are returned as `Err` so the rejection
/// boundary can be tested without mutating GC-root bookkeeping. Structural
/// validation and lifecycle guard violations still abort; the public
/// `Runtime::dispose_cell` wrapper preserves that aborting contract.
fn Runtime::try_dispose_cell(
self : Runtime,
cell_id : CellId,
) -> Result[Unit, DisposeError] {
guard @kernel.validate_cell_for_dispose(self.core, cell_id) else {
return Ok(())
}
@kernel.check_dispose_guard(self.core, cell_id)
match self.cell_lifecycle[cell_id.id].preflight_dispose_cell(self, cell_id) {
Some(error) => Err(error)
None => {
self.cell_lifecycle[cell_id.id].dispose_cell(self, cell_id)
@kernel.drop_gc_root(self.core, cell_id)
Ok(())
}
}
}
///|
pub fn Runtime::dispose_cell(self : Runtime, cell_id : CellId) -> Unit {
match self.try_dispose_cell(cell_id) {
Ok(_) => ()
Err(DisposeError::Rejected(error)) => abort(error)
}
}
///|
/// Increments the observer reference count for a cell.
/// Returns the previous count (0 means this is the first observer).
fn Runtime::add_gc_root(self : Runtime, id : CellId) -> Int {
@kernel.add_gc_root(self.core, id)
}
///|
/// Decrements the observer reference count for a cell.
/// Removes the entry when count reaches 0. Returns remaining count.
fn Runtime::remove_gc_root(self : Runtime, id : CellId) -> Int {
@kernel.remove_gc_root(self.core, id)
}
///|
fn Runtime::guard_collection_safe(self : Runtime) -> Unit {
guard self.static_recompute_depth == 0 else {
abort("gc: cannot run during static Derived recompute")
}
guard self.core.callback_depth == 0 else {
abort("gc: cannot run during callback")
}
guard !self.event_broadcast_hook.draining &&
!self.runtime_evaluation_event_hook.draining else {
abort("gc: cannot run during event drain")
}
guard !self.has_pending_events() else {
abort("gc: cannot run while events are buffered")
}
}
///|
/// Runs mark-and-sweep garbage collection on the dependency graph.
///
/// Dispose dispatch is injected as a closure closing over
/// `self.dispose_cell`, so per-kind CellLifecycle dispatch remains
/// reachable from the kernel sweep loop. Collection aborts unless the runtime
/// is between operations, including callback and event-drain boundaries.
pub fn Runtime::gc(self : Runtime) -> Unit {
self.guard_collection_safe()
@kernel.gc(self.core, fn(id) { self.dispose_cell(id) })
if self.has_pending_events() {
self.drain_pending_events_if_idle()
}
}
///|
/// Creates an input cell owned by this runtime.
pub fn[T] Runtime::input(
self : Runtime,
initial : T,
durability? : Durability = Low,
label? : String,
) -> Input[T] {
Input(self, initial, durability~, label?)
}