plushie/tree

Tree operations: normalization, diffing, and search.

After view(model) produces a Node tree, normalize applies scoped IDs and resolves a11y references. After each update cycle, diff compares old and new normalized trees to produce PatchOp lists for the wire protocol.

Types

Cache of memo-ized subtrees from the previous render cycle. Maps scoped memo ID to the dependency value, normalized subtree, and the widget registry entries and window IDs accumulated while normalizing that subtree. On cache hit (same dependency), the subtree and its accumulated data are restored without re-normalizing.

pub type MemoCache =
  dict.Dict(String, MemoCacheEntry)

A single entry in the memo cache.

pub type MemoCacheEntry {
  MemoCacheEntry(
    dep: dynamic.Dynamic,
    node: node.Node,
    registry: dict.Dict(String, widget.RegistryEntry),
    windows: set.Set(String),
  )
}

Constructors

Result of a full view normalization including accumulated state.

pub type NormalizeResult {
  NormalizeResult(
    tree: node.Node,
    memo_cache: dict.Dict(String, MemoCacheEntry),
    registry: dict.Dict(String, widget.RegistryEntry),
    windows: set.Set(String),
  )
}

Constructors

Values

pub fn diff(
  old: node.Node,
  new: node.Node,
) -> List(patch.PatchOp)

Compare two normalized trees and return a list of patch operations that transform old into new.

Paths are lists of integer child indices from the root. For example, [0, 2] means “root’s first child, then that node’s third child”.

The algorithm uses O(n) set comparison for reorder detection rather than O(n^2) LCS. When children are reordered, the entire node is replaced rather than computing minimal moves, a deliberate simplicity-over-optimality tradeoff matching the reference implementation.

pub fn empty_memo_cache() -> dict.Dict(String, MemoCacheEntry)

Create an empty memo cache for the first render cycle.

pub fn exists(tree: node.Node, id: String) -> Bool

Check whether a node with the given ID exists anywhere in the tree.

pub fn find(
  tree: node.Node,
  id: String,
) -> option.Option(node.Node)

Find a node by its ID (depth-first). Returns the first match.

Matches against the full ID first. If no match is found and the target does not contain “/”, falls back to matching the local segment (the part after the last “/”) of each node’s ID.

pub fn find_all(
  tree: node.Node,
  predicate: fn(node.Node) -> Bool,
) -> List(node.Node)

Collect all nodes matching a predicate (depth-first order).

pub fn ids(tree: node.Node) -> List(String)

Return all node IDs in depth-first order.

pub fn normalize(node: node.Node) -> node.Node

Normalize a node tree by applying scoped IDs and resolving a11y references. Call this on the output of view(model) before diffing.

Scoping rules:

  • Window nodes keep bare IDs. Their children get window#child_id.
  • Named (non-empty-ID) non-window nodes create scope boundaries. Deeper descendants get window#parent/child (the # separator only appears at the window boundary).
  • Nodes with an empty id are assigned an auto-ID of the form auto:<kind>:<index> and are transparent to scoping (like other auto-IDs).
pub fn normalize_view(
  node: node.Node,
  registry: dict.Dict(String, widget.RegistryEntry),
  prev_memo_cache: dict.Dict(String, MemoCacheEntry),
) -> Result(NormalizeResult, String)

Normalize a top-level app view and enforce explicit windows.

Accepts the previous render cycle’s memo cache and returns the normalized tree, new memo cache, accumulated widget registry, and detected window IDs. The registry and windows are collected during normalization itself, eliminating separate tree walks.

The runtime wraps the list-of-windows returned by view into a single tree root before calling this function, so the root here is either a window node, a synthetic container holding only window children, or an empty container representing [].

pub fn normalize_with_memo(
  node: node.Node,
  registry: dict.Dict(String, widget.RegistryEntry),
  prev_memo_cache: dict.Dict(String, MemoCacheEntry),
) -> NormalizeResult

Normalize with a widget registry and memo cache. Returns the normalized tree, new memo cache, accumulated widget registry, and detected window IDs.

pub fn normalize_with_registry(
  node: node.Node,
  registry: dict.Dict(String, widget.RegistryEntry),
) -> node.Node

Normalize with a widget registry. Widget placeholders in the tree are rendered using stored state from the registry.

pub fn view_list_to_tree(windows: List(node.Node)) -> node.Node

Collapse a list of top-level window nodes into a single tree root.

[] becomes an empty container (loading, transition, error screens). [single] promotes the single window to the root directly. Multiple entries wrap under a synthetic root container so the diff and normalize pipeline can treat the tree uniformly. Mirrors the Elixir runtime’s internal wrapping at plushie-elixir/lib/plushie/tree.ex.

Search Document