Sign in

    #dowdiness/incr/types

    Pure, zero-dependency value types used by dowdiness/incr.

    Consumers normally import dowdiness/incr, which re-exports everything in this package. Import dowdiness/incr/types directly only when an API needs these value types without pulling in the incremental engine.

    #Contents

    • Revision tracking: Revision, HasChangedAt, BackdateEq
    • Durability: Durability, GcRole
    • Errors: ReadError, CycleError
    • Identifiers: RuntimeId, CellId, ListenerId, RuleId, AccumulatorId, InternId
    • Interning: InternTable

    #See also

    BackdateEq

    pub(open) trait BackdateEq : HasChangedAt {
    fn backdate_equal(Self, Self) -> Bool = _
    }

    A backdate-aware equality check for memo outputs.

    The default implementation compares changed_at revisions: two values are considered "backdate equal" (i.e., the memo should not advance its changed_at) when both carry the same revision stamp. Override to provide custom logic.

    Supertrait

    Requires HasChangedAt — the type must expose a changed_at revision.

    HasChangedAt

    pub(open) trait HasChangedAt {
    fn changed_at(Self) -> Revision
    }

    A type that carries its own changed_at revision stamp.

    Implement this on value types that embed a Revision tracking when their content last changed. Used in combination with BackdateEq to enable O(1) backdate decisions based on revision comparison rather than O(N) structural equality.

    CycleError

    pub suberror CycleError {
    CycleDetected(CellId, Array[CellId], Array[String?])
    }

    Error type for cycle detection during memo computation.

    Returned by get_result() methods when a memo transitively depends on itself. Use cell(), path(), and format_path() to inspect or render the cycle. Labels are snapshotted at construction time so format_path is pure-value — rendering needs no runtime handle and reflects the cell labels at the moment the cycle was detected, even if those cells are later renamed or disposed.

    Example

    match memo.get_result() {
    Ok(value) => println("Got: " + value.to_string())
    Err(err) => {
    println("Cycle at cell " + err.cell().to_string())
    println(err.format_path())
    }
    }

    CycleError::cell

    fn CycleError::cell(self : CycleError) -> CellId

    CycleError::format_path

    fn CycleError::format_path(self : CycleError) -> String

    Formats the cycle path as a human-readable string, using captured labels when available and falling back to Cell[N] otherwise. Output longer than MAX_CYCLE_DISPLAY_STEPS entries is truncated with "→ ...".

    Example Output

    Cycle detected: self_ref → dep → self_ref

    CycleError::new

    fn CycleError::new(cell : CellId, path : Array[CellId], labels : Array[String?]) -> CycleError

    Construct a CycleError from its parts. Exposed so packages that detect cycles (and have access to a runtime for label lookup) can build the error without depending on variant-constructor visibility.

    Library-internal. The only intended caller is the kernel's cycle detection (cells/internal/kernel/cycle.mbt); consumers should never construct a CycleError themselves. It stays pub only because MoonBit has no visibility level that admits a sibling package while excluding external modules (attempted and recorded during the 2026-07-05 Phase 1 types cleanup).

    Invariant: labels.length() == min(path.length(), MAX_CYCLE_DISPLAY_STEPS). The caller must uphold this — format_path assumes path[i] has a matching labels[i] for every i it renders.

    CycleError::path

    fn CycleError::path(self : CycleError) -> Array[CellId]

    Full dependency path leading to the cycle, untruncated. The path may include cells outside the cycle itself (e.g., entry points). Use the repeated cell to identify the cycle boundary.

    Returns a fresh copy: mutating it cannot desynchronize the stored path from the labels snapshot that format_path indexes by position.

    AccumulatorId

    pub(all) struct AccumulatorId {
    runtime_id : RuntimeId
    id : Int
    } derive(Eq,
    Debug
    )

    A unique identifier for an Accumulator in the runtime.

    Monotonically allocated by the runtime; never reused across dispose/new. Stale references to disposed accumulators resolve to "slot disposed" at verify time rather than aliasing to a recycled slot.

    Each AccumulatorId is scoped to its originating runtime via runtime_id.

    CellId

    pub(all) struct CellId {
    runtime_id : RuntimeId
    id : Int
    } derive(Eq,
    Debug
    )

    A unique identifier for a cell (signal or memo) in the runtime.

    Cell IDs are monotonically increasing integers allocated by the runtime. They serve as direct array indices for O(1) cell lookup.

    Each CellId is scoped to its originating runtime via runtime_id. This prevents cells from different runtimes being incorrectly queried against each other.
    impl Hash for CellId
    impl Show for CellId

    Durability

    pub(all) enum Durability {
    Low
    Medium
    High
    } derive(Compare, Eq,
    Debug
    )

    Classifies how often an input is expected to change.

    Durability enables a powerful optimization: when only low-durability inputs change, memos that depend solely on high-durability inputs skip verification entirely.

    Levels

    • Low: Frequently changing values (user input, source text)
    • Medium: Moderately stable values
    • High: Rarely changing values (configuration, schemas)

    Inherited Durability

    Memos inherit the minimum durability of their dependencies. A memo depending on both Low and High durability signals has Low durability.
    impl Show for Durability

    Durability::index

    fn Durability::index(self : Durability) -> Int

    Returns the numeric index of this durability level.

    Used internally for array-based durability tracking. Low has the smallest index (0), High has the largest (2). The invariant Durability::High.index() + 1 == DURABILITY_COUNT must hold — if a new variant is added, both this match and DURABILITY_COUNT must be updated together.

    Returns

    • Low → 0
    • Medium → 1
    • High → 2

    GcRole

    pub(all) enum GcRole {
    Source
    Interior
    Root
    } derive(Eq,
    Debug
    )

    Categorizes a cell's role in garbage collection.

    • Source: Input cells (signals, relations) — no upstream deps, never collected
    • Interior: Derived cells (memos, reactives) — has deps, collectible when unobserved
    • Root: Terminal cells (effects) — keeps upstream alive, never collected
    impl Show for GcRole

    InternId

    pub struct InternId {
    index : Int
    } derive(Compare, Eq,
    Debug
    )

    A stable integer key returned by InternTable::intern.

    Same value always maps to the same InternId within a table. IDs are monotonically increasing and can be used as array indices or MemoMap keys.
    impl Hash for InternId
    impl Show for InternId

    InternTable

    pub struct InternTable[T] {
    // private fields
    }

    A grow-only interning table that assigns stable InternId keys to values.

    Interning guarantees that equal values always receive the same InternId, enabling cheap identity comparison (id1 == id2) instead of deep structural equality. Used by the bidirectional type-checker as MemoMap keys for cross-revision cache stability.

    InternTable::get

    fn[T] InternTable::get(self : InternTable[T], id : InternId) -> T

    Returns the value associated with id.

    Panics if id was not produced by this table.

    InternTable::intern

    fn[T : Hash + Eq] InternTable::intern(self : InternTable[T], value : T) -> InternId

    Returns the InternId for value, inserting it if not already present.

    InternTable::len

    fn[T] InternTable::len(self : InternTable[T]) -> Int

    Returns the number of unique values interned so far.

    InternTable::new

    fn[T : Hash + Eq] InternTable::new() -> InternTable[T]

    ListenerId

    pub(all) struct ListenerId {
    runtime_id : RuntimeId
    id : Int
    } derive(Eq, Hash,
    Debug
    )

    A handle identifying a single runtime-global listener registration.

    Returned by the additive listener APIs (Runtime::add_on_change_listener, Runtime::add_derived_event_listener) and passed back to their remove_* counterparts to detach a specific listener.

    An id pairs the originating RuntimeId with an allocation number drawn from a single per-runtime monotonic counter shared across both listener registries. The counter makes ids unique across the two registries of one runtime; the RuntimeId makes them unique across runtimes (the bare counter alone would collide — the first listener of two runtimes would both be number 0). Together they guarantee that passing an id to the wrong registry, or to a different runtime, is a no-op rather than an accidental removal of an unrelated hook.

    Like RuntimeId, a ListenerId is an introspection/debug identity — compare it for equality or use it as a map key; it is not a stable application key across program runs and carries no ordering or arithmetic meaning.
    impl Show for ListenerId

    ListenerId::ListenerId

    fn ListenerId::ListenerId(runtime_id : RuntimeId, id : Int) -> ListenerId

    Pairs a RuntimeId with a raw listener-allocation number as a ListenerId.

    ReactiveId

    pub(all) struct ReactiveId[T] {
    id : CellId
    }

    ReadError

    pub enum ReadError {
    Cycle(CycleError)
    Disposed(CellId)
    }

    Error type for the public read channel.

    A read asks for a stored graph snapshot, so the only failures it can report are mechanism failures — intrinsic to the read itself and unrecoverable by any domain reader:

    • Cycle — the cell transitively depends on itself.
    • Disposed — the cell being read has been disposed.

    Domain fallibility (a parse error, a validation failure) is not a read error: it belongs in the value as Result[V, E] (see Derived::fallible), where it is cached, change-detected, and replayed like any other value. A retained compute raise Failure is a defect, not a domain error, and still aborts.

    Cross-runtime misuse and strict-context misuse are programmer defects and abort rather than surfacing here.

    See docs/design/specs/2026-05-28-honest-read-error-ownership.md.
    impl Show for ReadError

    ReadError::cell

    fn ReadError::cell(self : ReadError) -> CellId

    Returns the cell this read error concerns: the cell at which the cycle was detected, or the disposed cell that was read.

    ReadError::cycle

    fn ReadError::cycle(e : CycleError) -> ReadError

    Constructs a cycle read error. Exposed so the cells package (which detects cycles) can build a ReadError without depending on variant-constructor visibility — mirrors CycleError::new.

    ReadError::disposed

    fn ReadError::disposed(id : CellId) -> ReadError

    Constructs a disposed-cell read error.

    ReadError::format_path

    fn ReadError::format_path(self : ReadError) -> String

    Renders this read error as a human-readable string. For a cycle, delegates to CycleError::format_path; for a disposed read, names the cell.

    ReadError::is_cycle

    fn ReadError::is_cycle(self : ReadError) -> Bool

    Returns true if this is a cycle error.

    ReadError::is_disposed

    fn ReadError::is_disposed(self : ReadError) -> Bool

    Returns true if this is a disposed-cell error.

    ReadError::path

    fn ReadError::path(self : ReadError) -> Array[CellId]

    Returns the cell path this read error concerns: the full dependency path leading to the cycle, or the single disposed cell that was read.

    Revision

    pub struct Revision {
    value : Int
    } derive(Compare, Default, Eq,
    Debug
    )

    A monotonically increasing revision counter representing the system's logical clock.

    Revisions are bumped each time an input signal changes. Every cell tracks two revision timestamps:

    • changed_at: When this cell's value last actually changed
    • verified_at: When this cell was last confirmed up-to-date

    A cell is stale when verified_at < current_revision. A cell has changed (relative to an observer) when changed_at > observer.verified_at.

    Revision::initial

    fn Revision::initial() -> Revision

    Returns the initial revision (revision 0).

    All cells start with changed_at and verified_at set to this value.

    Returns

    The initial revision

    Revision::next

    fn Revision::next(self : Revision) -> Revision

    Returns the next revision in sequence.

    Returns

    A new revision with value incremented by 1

    RuleId

    pub(all) struct RuleId {
    id : CellId
    }

    RuntimeId

    pub(all) struct RuntimeId {
    id : Int
    } derive(Eq, Hash,
    Debug
    )

    A unique identifier for a Runtime.

    Runtime ids are monotonically allocated when a Runtime is created and scope every CellId / AccumulatorId to its originating runtime.

    A RuntimeId is an introspection / debug identity: compare it for equality ("are these two runtimes the same?"), use it as a map key, or display it. It is not a stable application key across program runs, and carries no ordering or arithmetic meaning — that is why it is a nominal wrapper rather than a bare Int.
    impl Show for RuntimeId

    RuntimeId::RuntimeId

    fn RuntimeId::RuntimeId(id : Int) -> RuntimeId

    Wraps a raw runtime-allocation number as a RuntimeId.

    DURABILITY_COUNT

    let DURABILITY_COUNT : Int

    The number of durability levels (3: Low, Medium, High).

    Must equal Durability::High.index() + 1. Adding a new Durability variant requires updating both Durability::index() and this constant; failing to do so will silently under-size the durability_last_changed array in Runtime, causing incorrect fast-path revision checks.

    MAX_CYCLE_DISPLAY_STEPS

    let MAX_CYCLE_DISPLAY_STEPS : Int

    Maximum number of path entries rendered by CycleError::format_path, and the number of labels snapshotted at error-construction time.

    Cycles longer than this are truncated in the formatted output with "→ ...", but CycleError::path() still returns the full untruncated sequence.