///|
/// Structured metadata about a cell in the dependency graph.
///
/// This structure provides a uniform view of both Input and Derived cells.
/// For inputs, the `dependencies` array will be empty.
///
/// # Fields
///
/// - `label`: Optional human-readable name for debugging and introspection
/// - `id`: The unique identifier for this cell
/// - `changed_at`: When this cell's value last actually changed
/// - `verified_at`: When this cell was last confirmed up-to-date
/// - `durability`: How often this cell is expected to change
/// - `dependencies`: Cell IDs this cell depends on (empty for inputs)
pub(all) struct CellInfo {
label : String?
id : CellId
changed_at : Revision
verified_at : Revision
durability : Durability
dependencies : Array[CellId]
subscribers : Array[CellId]
}
///|
/// Snapshot a HashSet[CellId] into an Array[CellId].
fn snapshot_subscribers(
subscribers : @hashset.HashSet[CellId],
) -> Array[CellId] {
let result : Array[CellId] = []
for sub in subscribers {
result.push(sub)
}
result
}
///|
/// Returns structured metadata for any cell (input or derived).
///
/// This method provides uniform introspection access to cell metadata regardless of
/// whether the cell is an Input or Memo. It returns CellInfo containing the cell's
/// ID, type, revision information, durability, and dependency list.
///
/// # Parameters
///
/// - `id`: The CellId to query (obtained via Input::id() or Memo::id())
///
/// # Returns
///
/// - `Some(CellInfo)`: If the cell ID is valid and points to an active cell
/// - `None`: If the cell ID is out of bounds or points to an unused slot
///
/// # Usage
///
/// ```moonbit nocheck
/// let rt = Runtime()
/// let sig = Input(rt, 42)
/// let derived = Derived(rt, fn() { sig.get() * 2 })
/// let _ = derived.read_or_abort() // Force computation
///
/// // Query input metadata
/// match rt.cell_info(sig.id()) {
/// Some(info) => {
/// println("Input changed at: \{info.changed_at}")
/// println("Dependencies: \{info.dependencies.length()}")
/// }
/// None => println("Cell not found")
/// }
///
/// // Query derived metadata
/// match rt.cell_info(derived.id()) {
/// Some(info) => {
/// println("Derived durability: \{info.durability}")
/// println("Depends on \{info.dependencies.length()} cells")
/// }
/// None => println("Cell not found")
/// }
/// ```
///
/// # Notes
///
/// - The dependency array is a copy; modifying it does not affect the runtime
/// - For inputs, the dependencies array is always empty
/// - For derived cells, dependencies are populated after the first computation
/// - The method performs bounds checking and returns None for invalid IDs
pub fn Runtime::cell_info(self : Runtime, id : CellId) -> CellInfo? {
if !self.validate_cell_soft(id) {
return None
}
let ops = self.core.cell_ops[id.id]
match self.core.cell_index[id.id] {
PullInput(_) => {
let ca = ops.changed_at()
Some(CellInfo::{
label: ops.label(),
id,
changed_at: ca,
// Inputs are always up-to-date after a set; verified_at == changed_at
verified_at: ca,
durability: ops.durability(),
dependencies: [],
subscribers: snapshot_subscribers(ops.subscribers()),
})
}
PullMemo(idx) | HybridMemo(idx) => {
let memo = self.pull.memos[idx]
Some(CellInfo::{
label: ops.label(),
id,
changed_at: ops.changed_at(),
verified_at: memo.verified_at,
durability: ops.durability(),
dependencies: memo.dependencies.copy(),
subscribers: snapshot_subscribers(ops.subscribers()),
})
}
_ => None
}
}
///|
/// Returns the cell IDs that depend on the given cell (reverse edges).
///
/// This enables introspection of the dependency graph in both directions.
/// The returned array is a snapshot; modifying it does not affect the runtime.
///
/// Returns an empty array if the cell ID is invalid, disposed, out of bounds,
/// or belongs to a different runtime — matching `cell_info` semantics.
///
/// # Parameters
///
/// - `id`: The cell to query
///
/// # Returns
///
/// Array of CellIds that have `id` in their dependency list, or empty
/// array if the cell ID is not valid for this runtime or has been disposed
pub fn Runtime::dependents(self : Runtime, id : CellId) -> Array[CellId] {
if !self.validate_cell_soft(id) {
return []
}
if self.is_cell_disposed(id) {
return []
}
snapshot_subscribers(self.core.cell_ops[id.id].subscribers())
}
///|
/// Number of GC roots currently anchoring `id` — i.e. how many `Watch`
/// handles or `gc_root = Root` cells reference it. Returns
/// `0` for unknown or disposed cells (matches the `Runtime::dependents`
/// soft-fail idiom).
///
/// Phase 1 coordinator use: registration-time mode-1 check that each
/// protected cell is watch-rooted before the editor is published.
pub fn Runtime::gc_root_count(self : Runtime, id : CellId) -> Int {
match self.core.gc_root_counts.get(id) {
Some(n) => n
None => 0
}
}