README

#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.