Reactive.js

The complete source of src/Reactive.js, exactly as it ships, in reading order. Every line is present; the file is split at its own section banners.

Reactive.js - the self-contained reactive framework

// ============================================================================
// Reactive.js - the self-contained reactive framework
// ============================================================================
//
// One dependency-free file that defines the global `Reactive` and nothing else.
// Every other file in this directory (Dom, Content, Layout, Button, Input, Maths,
// Clip.*) is authored AGAINST this: it calls Reactive.Module/Function/Type/Union/
// Field/Variant to declare things, and the runtime (State/Calculate/Reverse/Binding/
// Set/Run/If/Each) to make them live.
//
// Two layers live here (see docs/13-reactive-js-implementation.md):
//   1. the runtime  - state, calculations, reverse calculations, control flow,
//                     locking and errors. A reshape of the proven phase-zero
//                     implementation (program/phase-zero/.../Reactive/native/js).
//   2. reification  - Module/Function/Type/Union/Field/Variant and the dotted global
//                     namespace they build.
// Contexts, the typed reversible Proxy accessors, and the driver infrastructure the
// DOM driver (Dom.js) realises are layered on top in later sections.
//
// The internal value model (ReactiveValue and friends) is PRIVATE to this file:
// consumers never hold one directly, they pass bindings opaquely or hold Proxies.
//
// Reversibility (the key feature), in one paragraph: a binding carries a reverse
// callback. Reactive.Set(binding, v) calls it synchronously with the new native
// value; the callback is plain forward code that ends by Set-ting OTHER bindings.
// Those sets fan out into a TREE whose leaves are State writes. If any leaf is locked
// (an async step in flight) the whole reverse process is kept "unfinished" and its
// destination states are marked locked until a driver unlocks them. That single
// mechanism is the entire async/transaction/locking story.
// ----------------------------------------------------------------------------

;(function () {
  'use strict'

Section 1 - internal value model (private)

  // ==========================================================================
  // Section 1 - internal value model (private)
  // ==========================================================================
  //
  // One container, ReactiveValue { optionalGettable, optionalSettable }:
  //   gettable only  = a constant or a Calculate result (read only)
  //   settable only  = a Reverse(cb) write sink
  //   both           = a two-way binding (a State, or Binding(get, set))

  // An error attached to a lock. optionalRetryFlagId, when set, is a settable id:
  // Set that flag to true to trigger a retry.
  class ReactiveError {
    constructor(message, optionalRetryFlagId) {
      this.message = message
      this.optionalRetryFlagId = optionalRetryFlagId
    }
  }

  // Present on a gettable when the value is locked. optionalError null = loading.
  class ReactiveLock {
    constructor(optionalError) {
      this.optionalError = optionalError
    }
  }

  // The readable half. value is the native payload; it may itself be undefined for
  // a nullable type, independent of whether the value is gettable.
  //
  // TWO locks, distinct in effect:
  //   optionalLock      - the READ lock: the value is not ready (loading). When a UI area
  //                       reads it to display, a loading spinner appears around that area.
  //   optionalWriteLock - the WRITE lock: the value can still be read/displayed, but it may
  //                       not be SET right now (an ongoing reverse process reads or writes
  //                       it). A settable control bound to it (Button.Set, an input) is
  //                       disabled. A reverse destination carries BOTH.
  class ReactiveGettable {
    constructor(optionalLock, value, optionalWriteLock) {
      this.optionalLock = optionalLock
      this.value = value
      this.optionalWriteLock = optionalWriteLock
    }
  }

  // The writable half. id is the stable settable id; optionalReverseCallback, when
  // present, is run synchronously by Reactive.Set (a Reverse sink); when absent the
  // settable is a State leaf and Set records the write.
  class ReactiveSettable {
    constructor(id, optionalReverseCallback) {
      this.id = id
      this.optionalReverseCallback = optionalReverseCallback
    }
  }

  class ReactiveValue {
    constructor(optionalGettable, optionalSettable) {
      this.optionalGettable = optionalGettable
      this.optionalSettable = optionalSettable
    }
  }

  // ---- lock trees ------------------------------------------------------------
  //
  // A gettable's optionalLock / optionalWriteLock is a LOCK TREE (or null): locks scoped to
  // paths in the value's field structure. A node is { direct, fields }:
  //   direct - a ReactiveLock (carrying optionalError) locking THIS exact path, or null
  //   fields - { name: LockTree } - locks on descendant fields, siblings independent
  // PRUNED invariant: a subtree exists only when it, or something below it, is directly
  // locked - so a NON-NULL tree always means "something at or below this path is locked".
  // That is exactly the field-level rule: a lock on an object covers every field DOWN
  // (lockChild pushes `direct` onto each field); a locked field makes its container locked
  // UP (the container's tree is non-null) while its SIBLINGS stay free (their subtrees are
  // absent). A whole-value lock is just a leaf: { direct: lock, fields: {} }.

  function lockLeaf(reactiveLock) {
    return reactiveLock == null ? null : { direct: reactiveLock, fields: {} }
  }

  // Slice a tree DOWN to one field: an ancestor's direct lock pushes onto the field (a locked
  // object locks each of its fields), keeping any finer structure beneath it.
  function lockChild(tree, name) {
    if (tree == null) {
      return null
    }
    const own = tree.fields[name] == null ? null : tree.fields[name]
    if (tree.direct != null) {
      const fields = own == null ? {} : own.fields
      return { direct: tree.direct, fields: fields }
    }
    return own
  }

  // Wrap a subtree under a path (root..leaf): path ['location','lat'] + a leaf lock becomes
  // { fields: { location: { fields: { lat: <leaf> } } } }. An empty path returns the subtree.
  function lockScope(path, subtree) {
    if (subtree == null) {
      return null
    }
    let node = subtree
    for (let i = path.length - 1; i >= 0; i--) {
      const fields = {}
      fields[path[i]] = node
      node = { direct: null, fields: fields }
    }
    return node
  }

  // Union of two trees (all locked paths of both). A direct error beats a direct loading lock.
  function lockMerge(a, b) {
    if (a == null) {
      return b
    }
    if (b == null) {
      return a
    }
    const aErr = a.direct != null && a.direct.optionalError != null
    const bErr = b.direct != null && b.direct.optionalError != null
    const direct = aErr ? a.direct : (bErr ? b.direct : (a.direct != null ? a.direct : b.direct))
    const fields = {}
    for (const k of Object.keys(a.fields)) {
      fields[k] = a.fields[k]
    }
    for (const k of Object.keys(b.fields)) {
      fields[k] = fields[k] == null ? b.fields[k] : lockMerge(fields[k], b.fields[k])
    }
    return { direct: direct, fields: fields }
  }

  // The first error anywhere in the tree (direct before descendants), or null.
  function lockFirstError(tree) {
    if (tree == null) {
      return null
    }
    if (tree.direct != null && tree.direct.optionalError != null) {
      return tree.direct.optionalError
    }
    for (const k of Object.keys(tree.fields)) {
      const found = lockFirstError(tree.fields[k])
      if (found != null) {
        return found
      }
    }
    return null
  }

  // Collect every error in the tree into `into` (for combined retry-flag handling).
  function lockAllErrors(tree, into) {
    if (tree == null) {
      return into
    }
    if (tree.direct != null && tree.direct.optionalError != null) {
      into.push(tree.direct.optionalError)
    }
    for (const k of Object.keys(tree.fields)) {
      lockAllErrors(tree.fields[k], into)
    }
    return into
  }

  // A stable-identity tree node. branches is name -> child scope-item list (If/Each/
  // Branches). It holds no values; persisted values live in ReactiveProgram.values.
  class ReactiveScopeItem {
    constructor(id, branches) {
      this.id = id
      this.branches = branches
    }
  }

  // A forward or reverse process: a callback plus the root scope items that give it
  // stable ids across runs.
  class ReactiveProcess {
    constructor(callback, rootScopeItems) {
      this.callback = callback
      this.rootScopeItems = rootScopeItems
    }
  }

  // The whole program across runs. reverseProcesses is settableId -> unfinished
  // process; values is stableId -> ReactiveValue (the state memory).
  class ReactiveProgram {
    constructor(forwardProcess, reverseProcesses, values) {
      this.forwardProcess = forwardProcess
      this.reverseProcesses = reverseProcesses
      this.values = values
    }
  }

  // A reactive array value and its keyed elements (used by Each and, later, by the
  // array accessors).
  class ReactiveArray {
    constructor(elements) {
      this.elements = elements
    }
  }

  class ReactiveArrayElement {
    constructor(key, value) {
      this.key = key
      this.value = value
    }
  }

Section 1b - security (strict base checks)

  // ==========================================================================
  // Section 1b - security (strict base checks)
  // ==========================================================================
  //
  // The always-on structural checks: a value is gettable / settable / reverse /
  // binding as required. Strict TYPE validation (against reified descriptors) layers
  // on top of these in a later section.

  const Security = {
    Gettable(valueOrReactiveValue) {
      if (!(valueOrReactiveValue instanceof ReactiveValue)) {
        return
      }
      if (valueOrReactiveValue.optionalGettable == null) {
        throw new Error('Value is not gettable: it is settable-only')
      }
    },

    OptionalGettable(optionalValueOrReactiveValue) {
      if (optionalValueOrReactiveValue == null) {
        return
      }
      Security.Gettable(optionalValueOrReactiveValue)
    },

    Settable(reactiveValue) {
      if (!(reactiveValue instanceof ReactiveValue) || reactiveValue.optionalSettable == null) {
        throw new Error('Value is not settable')
      }
    },

    Reverse(reactiveValue) {
      Security.Settable(reactiveValue)
      if (reactiveValue.optionalSettable.optionalReverseCallback == null) {
        throw new Error('Value is not reverse-settable: it has no reverse callback')
      }
    },

    Binding(reactiveValue) {
      Security.Gettable(reactiveValue)
      Security.Settable(reactiveValue)
    },
  }

Section 2 - runtime engine (reshaped from phase-zero)

  // ==========================================================================
  // Section 2 - runtime engine (reshaped from phase-zero)
  // ==========================================================================
  //
  // Same names and semantics as the phase-zero JS runtime; renamed
  // optionalNativeReverseCallback -> optionalReverseCallback and dropped the
  // auto-generated banners. The algorithm (reverse-tree locking, slot-by-index stable
  // identity, forward lock/error combination) is unchanged.

  class Reactive {
    // ---- per-run engine state -------------------------------------------------

    // Source settable values for the current Run: program.values merged with state
    // updates and reverse batches.
    static _sourceValueMap = {}
    // Destination settable values gathered during the current Run (State/Reverse
    // persist into here).
    static _destinationValueMap = {}
    // Global counter for unique integer ids (scope-item and settable ids).
    static _nextIdCounter = 1
    // Values read via Reactive.Value during a Calculate, so Calculate can combine
    // their locks/errors into its output.
    static _calculateValues = []
    // Ids explicitly Set on State leaves inside the current reverse process (null
    // when outside a reverse process). Used to lock only the actual Set targets.
    static _reverseSetTargets = null
    // Values READ (via Reactive.Value) inside the current reverse process (null when
    // not in one). An unfinished reverse write-locks the STATE leaves among these - the
    // stable inputs it read from its closure - so nothing else changes them mid-flight.
    static _reverseReadValues = null
    // The lock tree of the value currently being Set (stashed by Set around a reverse
    // callback), so a field reverse can carry the incoming read-lock, scoped, up to the
    // backing state. null when the set value is unlocked or outside a Set.
    static _reverseInputLock = null
    // The field path (root..field) currently being written, threaded by the _objectField
    // reverses as a set cascades UP to its backing state; [] for a whole-value set.
    static _reverseFieldPath = []
    // Per set-target id, the list of field paths written in the current reverse process, so
    // the process can scope its WRITE lock to exactly those paths (null when outside one).
    static _reverseSetPaths = null
    // Slot-by-index scope-item lists for the current scope run.
    static _sourceScopeItems = []
    static _destinationScopeItems = []
    // Emission frames for Emit/Gather. One frame per running Reactive.Function; a
    // frame is channel -> [values]. Gather drains the top frame; on function return
    // un-gathered channels bubble into the caller's frame (see Section 6).
    static _emissionFrameStack = []
    // After a Run, the forward pass's root frame: whatever bubbled to the top with no
    // function Gather catching it. Drivers drain their channel from here (Section 7).
    static _rootEmissions = {}
    // After a Run, the value the forward callback RETURNED: a structural region's gathered
    // output as a { channel: [values] } map (e.g. { content: nodeList }). Mount FOLDS this into
    // _rootEmissions so structural output reaches its driver by the same path as an ambient
    // signal - there is no privileged channel, drivers all just drain their channel from root.
    // _lastCallbackResult is the scratch the shared _runCallback writes on every call;
    // _programResult is the forward callback's result, read once right after the forward pass.
    static _lastCallbackResult = null
    static _programResult = null

    // TYPE-CHECK MODE. When true (only during Reactive.Mount's static pass), the forward run is a
    // whole-program TYPE traversal, not a real render: it follows EVERY branch (an If runs its
    // body regardless of the condition; an Each runs its item callback once) so type-inconsistent
    // channels on any path are found before the program starts. Termination is by DEPTH, not by
    // function identity: scope functions like Dom are legitimately re-entrant (every child tag
    // re-enters Dom), so an identity guard would refuse to descend into the tree. Instead a call
    // stack deeper than _typeCheckDepthLimit is treated as runaway recursion (branch coverage
    // forces recursive branches that real data would stop) and the over-deep call returns a
    // permissive placeholder (any property is an empty array) so nothing crashes.
    static _typeChecking = false
    static _typeCheckDepth = 0
    static _typeCheckDepthLimit = 300

    // ---- run loop -------------------------------------------------------------

    // Core execution: apply updates, run pending + new reverse processes, run the
    // forward process, and return a new program (new scope tree, unfinished reverse
    // processes, fresh values map).
    static Run(program, optionalUpdates = null) {
      const oldSourceValueMap = Reactive._sourceValueMap
      const oldDestinationValueMap = Reactive._destinationValueMap
      const oldSourceScopeItems = Reactive._sourceScopeItems
      const oldDestinationScopeItems = Reactive._destinationScopeItems

      const updates = optionalUpdates == null ? {} : optionalUpdates

      const stateUpdates = Reactive._getStateUpdates(updates, program.values)
      Reactive._sourceValueMap = Reactive._mergeUpdates(program.values, stateUpdates)
      Reactive._destinationValueMap = {}

      const reverseUpdates = Reactive._getReverseUpdates(updates, program.values)
      const createdReverseProcesses = Reactive._createReverseProcesses(reverseUpdates)
      const reverseProcesses = Object.assign({}, program.reverseProcesses, createdReverseProcesses)

      const previousEmissionFrameStack = Reactive._emissionFrameStack
      // ONE root frame for the whole cycle. Reverse processes run first and emit into it
      // (so a reverse can hand async work - a request - to a driver), then the forward
      // pass emits into it too. Everything that bubbles to the top is the drivers' work.
      Reactive._emissionFrameStack = [{}]
      const nextReverseProcesses = Reactive._runReverseProcesses(reverseProcesses)
      const updatedForwardScopeItems = Reactive._runCallback(program.forwardProcess.rootScopeItems, program.forwardProcess.callback)
      // The forward callback is the outermost _runCallback and returns last, so its result
      // is what _lastCallbackResult holds now (nested Branches callbacks all returned earlier).
      Reactive._programResult = Reactive._lastCallbackResult
      Reactive._rootEmissions = Reactive._emissionFrameStack[0]
      Reactive._emissionFrameStack = previousEmissionFrameStack
      const updatedForwardProcess = new ReactiveProcess(program.forwardProcess.callback, updatedForwardScopeItems)

      const newValues = Reactive._destinationValueMap

      Reactive._sourceValueMap = oldSourceValueMap
      Reactive._destinationValueMap = oldDestinationValueMap
      Reactive._sourceScopeItems = oldSourceScopeItems
      Reactive._destinationScopeItems = oldDestinationScopeItems

      return new ReactiveProgram(updatedForwardProcess, nextReverseProcesses, newValues)
    }

    // Build a ReactiveProgram from a program callback.
    static Program(programCallback) {
      const rootItems = []
      const forwardProcess = new ReactiveProcess(programCallback, rootItems)
      return new ReactiveProgram(forwardProcess, {}, {})
    }

    // ---- control flow ---------------------------------------------------------

    // Create branches at the current scope position for conditional logic and
    // iteration. scopeCallback receives a scope(name, body) function.
    static Branches(branchCallback) {
      const destinationScopeItems = Reactive._destinationScopeItems
      const item = Reactive._nextSourceScopeItem()

      const scope = (scopeName, scopeCallback) => {
        const optionalBranchSourceScopeItems = item.branches[scopeName]
        const branchSourceScopeItems = optionalBranchSourceScopeItems == null ? [] : optionalBranchSourceScopeItems
        const branchDestinationScopeItems = Reactive._runCallback(branchSourceScopeItems, scopeCallback)
        item.branches[scopeName] = branchDestinationScopeItems
      }

      branchCallback(scope)
      destinationScopeItems.push(item)
    }

    // Run onTrue only when condition (raw or reactive boolean) is true. In type-check mode the
    // condition is ignored and the body is always entered, so the branch is covered.
    static If(condition, onTrue) {
      Reactive.Branches((scope) => {
        const rawValue = condition instanceof ReactiveValue
          ? (condition.optionalGettable == null ? false : condition.optionalGettable.value)
          : condition
        const conditionValue = typeof rawValue === 'boolean' ? rawValue : !!rawValue
        if (conditionValue || Reactive._typeChecking) {
          scope('if', () => {
            onTrue()
          })
        }
      })
    }

    // Iterate a ReactiveArray, one named branch scope per element (keyed by element
    // key). While the array is locked, itemCallback is not invoked.
    static Each(reactiveArray, itemCallback) {
      Security.Gettable(reactiveArray)

      const isLocked = Reactive.IsLocked(reactiveArray)
      const native = reactiveArray instanceof ReactiveValue
        ? (reactiveArray.optionalGettable == null ? null : reactiveArray.optionalGettable.value)
        : reactiveArray
      const list = isLocked || native == null ? [] : native.elements

      Reactive.Branches((scope) => {
        // Type-check mode: cover the item callback exactly once even when the array is empty at
        // startup, using a trusted placeholder element (its type is declared by the callback's
        // own Reactive.Argument, so the element is passed through untouched).
        if (Reactive._typeChecking && list.length === 0) {
          scope('typecheck', () => {
            const placeholder = Reactive.Create(undefined)
            placeholder[TYPE_SYMBOL] = { __typeCheckElement: true }
            itemCallback(placeholder)
          })
          return
        }
        for (const elementData of list) {
          scope(elementData.key, () => {
            const reactiveValue = Reactive.Create(elementData.value)
            itemCallback(reactiveValue)
          })
        }
      })
    }

    // ---- stable identity ------------------------------------------------------

    // A stable id for the current call-site position.
    static StableId() {
      const destinationScopeItems = Reactive._destinationScopeItems
      const item = Reactive._nextSourceScopeItem()
      destinationScopeItems.push(item)
      return item.id
    }

    // A stable string key for collections/contexts.
    static StableKey(optionalPrefix) {
      const id = Reactive.StableId()
      const key = id.toString()
      return optionalPrefix == null ? key : optionalPrefix + key
    }

    // ---- values ---------------------------------------------------------------

    // Extract the native value from a value or ReactiveValue. Inside a Calculate this
    // also records the value so its lock/error combines into the result.
    static Value(valueOrReactiveValue) {
      Security.Gettable(valueOrReactiveValue)
      if (!(valueOrReactiveValue instanceof ReactiveValue)) {
        return valueOrReactiveValue
      }
      const reactiveValue = valueOrReactiveValue
      Reactive._calculateValues.push(reactiveValue)
      if (Reactive._reverseReadValues != null) {
        Reactive._reverseReadValues.push(reactiveValue)
      }
      return reactiveValue.optionalGettable.value
    }

    // A mutable reactive state value (a two-way binding to a persisted leaf). CURRIED like
    // every other generic (Fn(type)(value)): the type is the generic parameter and is always
    // given - State(Text)("info"), State(Clip.Object.Data)(objectNative). For a loosely-typed
    // leaf use Reactive.Any: State(Reactive.Any)(0).
    static State(type) {
      return function (initialValue) {
        return Reactive._makeState(initialValue, type, false)
      }
    }

    // Like State, but a NEWLY created value starts locked (loading). On later runs the
    // persisted value is used - which a driver has typically unlocked with real data. This
    // is the async-initial pattern: Network.Web.Client.Request returns a Loading state that
    // its driver fills in, so the UI shows a spinner until the data arrives. Curried the same
    // way: Loading(Network.Web.Response)(empty).
    static Loading(type) {
      return function (initialValue) {
        return Reactive._makeState(initialValue, type, true)
      }
    }

    static _makeState(initialValue, optionalType, initialLocked) {
      const id = Reactive.StableId()
      const optionalExisting = Reactive._sourceValueMap[id]
      const descriptor = Reactive.DescriptorOf(optionalType)

      let value
      if (optionalExisting != null) {
        const existingGettable = new ReactiveGettable(optionalExisting.optionalGettable.optionalLock, optionalExisting.optionalGettable.value, optionalExisting.optionalGettable.optionalWriteLock)
        const existingSettable = new ReactiveSettable(optionalExisting.optionalSettable.id, undefined)
        value = new ReactiveValue(existingGettable, existingSettable)
      } else {
        const rawInitial = initialValue instanceof ReactiveValue ? initialValue.optionalGettable.value : initialValue
        if (descriptor != null) {
          validateAgainstDescriptor(rawInitial, descriptor, descriptor.name)
        }
        const optionalLock = initialLocked ? lockLeaf(new ReactiveLock(undefined)) : undefined
        const gettable = new ReactiveGettable(optionalLock, rawInitial)
        const settable = new ReactiveSettable(id, undefined)
        value = new ReactiveValue(gettable, settable)
      }

      Security.Settable(value)
      Reactive._destinationValueMap[id] = value

      if (descriptor != null) {
        return Reactive._typed(value, descriptor)
      }
      return value
    }

    // Normalise a value into a ReactiveValue. If already reactive, returned as-is;
    // otherwise wrapped as a constant (optionally locked / errored).
    static Create(value, optionalLocked, optionalErrorMessage, optionalRetryFlagId) {
      Security.Gettable(value)
      if (value instanceof ReactiveValue) {
        return value
      }
      const optionalError = optionalErrorMessage != null ? new ReactiveError(optionalErrorMessage, optionalRetryFlagId) : undefined
      const optionalLock = optionalLocked === true ? lockLeaf(new ReactiveLock(optionalError)) : undefined
      const gettable = new ReactiveGettable(optionalLock, value)
      return new ReactiveValue(gettable, undefined)
    }

    // Run a compute callback and combine the locks/errors of every input read via Reactive.Value
    // into a { value, optionalLock }. This is where the callback actually RUNS - in the
    // 'calculate' driver, not synchronously in the program. The callback must not throw.
    static _runCalculation(nativeCalculationCallback) {
      const previousCalculateValues = Reactive._calculateValues
      Reactive._calculateValues = []
      const result = nativeCalculationCallback()
      const calculateValues = Reactive._calculateValues
      Reactive._calculateValues = previousCalculateValues

      const resultReactive = Reactive.Create(result)
      const resultValue = resultReactive.optionalGettable.value
      const allValues = [resultReactive].concat(calculateValues)
      const trees = Reactive._getLocks(allValues)
      const errors = Reactive._getErrors(trees)
      const retryFlagIds = Reactive._getRetryFlagIds(errors)
      const isLocked = trees.length > 0
      const hasError = errors.length > 0
      const combinedRetryFlagId = Reactive._generateCombinedRetryFlagId(retryFlagIds)

      // A Calculate result is a fresh scalar (no field structure), so its lock is a leaf: it
      // is locked iff any input value is locked-at-its-path (its sliced tree is non-null).
      const optionalError = hasError ? new ReactiveError('Multiple errors', combinedRetryFlagId) : undefined
      const optionalLock = isLocked ? lockLeaf(new ReactiveLock(optionalError)) : undefined
      return { value: resultValue, optionalLock: optionalLock }
    }

    // Reactive.Calculate is REMOVED from the program surface - commented out completely. Arbitrary
    // inline computation is not allowed: every derived value must be a typed OPERATION with a
    // driver (Maths.Add, Text.Concat, ...), so the whole system is driver-based and statically
    // type-checkable. The lock-combining machinery survives as Reactive._runCalculation, which the
    // operation drivers use internally; there is no public entry point. A call site that still
    // reaches for it fails loudly (undefined is not a function), so remaining uses are found.
    //
    // static Calculate(nativeCalculationCallback) {
    //   const result = Reactive.Loading(Reactive.Any)(undefined)
    //   Reactive.Emit('calculate', { callback: nativeCalculationCallback, result: result })
    //   return result
    // }

    // ---- reverse (the core of two-way binding) --------------------------------

    // Create a settable-only value with a reverse callback, run when it is Set. Pair
    // it with a gettable via Binding to make a two-way binding.
    static Reverse(reverseCallback) {
      const id = Reactive.StableId()
      const settable = new ReactiveSettable(id, reverseCallback)
      const reactiveValue = new ReactiveValue(undefined, settable)
      Reactive._destinationValueMap[id] = reactiveValue
      return reactiveValue
    }

    // Combine a gettable value with a settable into a two-way binding.
    static Binding(gettable, settable) {
      Security.Gettable(gettable)
      Security.Settable(settable)
      return new ReactiveValue(gettable.optionalGettable, settable.optionalSettable)
    }

    // Set a value on a binding. A reverse sink runs its callback synchronously (which
    // typically Sets further bindings, forming the reverse tree); a State leaf records
    // the write and is tracked as a Set target for lock combination.
    static Set(reactiveValue, newValueOrReactiveValue) {
      Security.Settable(reactiveValue)

      const id = reactiveValue.optionalSettable.id
      const optionalReverseCallback = reactiveValue.optionalSettable.optionalReverseCallback

      const newReactiveValue = Reactive.Create(newValueOrReactiveValue)
      const newOptionalLock = newReactiveValue.optionalGettable.optionalLock
      const newValue = newReactiveValue.optionalGettable.value

      // Strict typing: if this binding carries a type, validate the new native value.
      // Skipped while locked (a locked value may carry a placeholder payload).
      const descriptor = reactiveValue[TYPE_SYMBOL]
      if (descriptor != null && newOptionalLock == null) {
        validateAgainstDescriptor(newValue, descriptor, descriptor.name)
      }

      if (optionalReverseCallback != null) {
        // Hand the incoming value's lock tree to the reverse (a field reverse scopes it under
        // its field name and carries it up); save/restore so nested Sets don't clobber it.
        const previousInputLock = Reactive._reverseInputLock
        Reactive._reverseInputLock = newOptionalLock
        try {
          optionalReverseCallback(newValue)
        } finally {
          Reactive._reverseInputLock = previousInputLock
        }
      } else {
        if (Reactive._reverseSetTargets != null) {
          Reactive._reverseSetTargets.push(id)
          // Record which field path of this state was written (threaded up by the field
          // reverses; [] for a whole-value set), so the reverse process scopes its write lock.
          if (Reactive._reverseSetPaths != null) {
            const existingPaths = Reactive._reverseSetPaths[id] == null ? [] : Reactive._reverseSetPaths[id]
            existingPaths.push(Reactive._reverseFieldPath.slice())
            Reactive._reverseSetPaths[id] = existingPaths
          }
        }
        const gettable = new ReactiveGettable(newOptionalLock, newValue)
        const settable = new ReactiveSettable(id, undefined)
        Reactive._destinationValueMap[id] = new ReactiveValue(gettable, settable)
      }
    }

    // PRIVATE. The reversible field step the typed Proxy accessors (section 5) are built on.
    // Forward: read the field, with its lock sliced from the object's lock tree so each field
    // is locked EXACTLY (an ancestor lock covers it, a sibling's lock does not). Reverse:
    // rebuild the object with the field replaced and Set the object, carrying the incoming
    // value's lock scoped under this field and threading this field onto the write path.
    // Not public: application code reaches fields through the Proxy dotted notation.
    static _objectField(object, rawFieldGetterFunction, rawFieldChangerFunction, fieldName) {
      Security.Gettable(object)
      const objectGettable = object instanceof ReactiveValue ? object.optionalGettable : null
      const objectNative = objectGettable == null ? object : objectGettable.value
      const rawField = rawFieldGetterFunction(objectNative)

      // CASE B (direct-backing): the field's value IS itself a settable binding (a State or
      // Reverse stuffed into a plain object). Return it directly, so reads and writes go
      // straight to that backing regardless of whether the container is settable. This is what
      // lets a settable field live inside a gettable-only object and just work.
      if (rawField instanceof ReactiveValue) {
        return rawField
      }

      // CASE A: an ordinary native field. Slice the object's lock trees down to this field.
      const objectReadTree = objectGettable == null ? undefined : objectGettable.optionalLock
      const objectWriteTree = objectGettable == null ? undefined : objectGettable.optionalWriteLock
      const fieldReadTree = lockChild(objectReadTree, fieldName)
      const fieldWriteTree = lockChild(objectWriteTree, fieldName)
      const fieldGettable = new ReactiveGettable(fieldReadTree, rawField, fieldWriteTree)

      if (object instanceof ReactiveValue && object.optionalSettable != null) {
        const settable = Reactive.Reverse((newFieldValue) => {
          const incomingLock = Reactive._reverseInputLock
          const currentGettable = object.optionalGettable
          const currentNative = currentGettable == null ? undefined : currentGettable.value
          const updatedNative = rawFieldChangerFunction(currentNative, newFieldValue)
          // Carry the incoming read-lock scoped under this field (a field set to a loading
          // value keeps THAT field, and it alone, read-locked at the backing state).
          const scopedLock = lockScope([fieldName], incomingLock)
          const updatedValue = new ReactiveValue(new ReactiveGettable(scopedLock, updatedNative), undefined)
          Reactive._reverseFieldPath.unshift(fieldName)
          try {
            Reactive.Set(object, updatedValue)
          } finally {
            Reactive._reverseFieldPath.shift()
          }
        })
        return new ReactiveValue(fieldGettable, settable.optionalSettable)
      }
      return new ReactiveValue(fieldGettable, undefined)
    }

    // ---- references -----------------------------------------------------------

    // The settable id of a binding, so it can be passed through contexts that would
    // otherwise unwrap the ReactiveValue.
    static Reference(settable) {
      Security.Settable(settable)
      return settable.optionalSettable.id
    }

    // The inverse of Reference: the ReactiveValue for a settable id. Throws on an
    // unknown id (a programming error).
    static Dereference(settableId) {
      const fromSource = Reactive._sourceValueMap[settableId]
      if (fromSource !== undefined) {
        return fromSource
      }
      const fromDestination = Reactive._destinationValueMap[settableId]
      if (fromDestination !== undefined) {
        return fromDestination
      }
      throw new Error('Reactive.Dereference: settable id ' + settableId + ' not found')
    }

    // ---- lock / error readers -------------------------------------------------

    static IsLocked(value) {
      if (!(value instanceof ReactiveValue)) {
        return false
      }
      return value.optionalGettable != null && value.optionalGettable.optionalLock != null
    }

    // Whether a value CAN be set at all: it carries a settable (a State or a reverse output, or
    // a field that routes to one), as opposed to a read-only value (a literal, a Calculate, a
    // gathered result). This is the first-class settability check, so read-only and settable
    // values can be mixed into any object fearlessly and a driver can ask "can I set this?"
    // without ever seeing an id. A control bound to a value that cannot be set is disabled, just
    // as one bound to a write-locked value is (see IsWriteLocked).
    static CanSet(value) {
      return value instanceof ReactiveValue && value.optionalSettable != null
    }

    // The WRITE lock: true when the value must not be SET right now (an ongoing reverse
    // process reads or writes it). It can still be read/displayed. A settable control
    // bound to a write-locked value is disabled by the driver.
    static IsWriteLocked(value) {
      if (!(value instanceof ReactiveValue)) {
        return false
      }
      return value.optionalGettable != null && value.optionalGettable.optionalWriteLock != null
    }

    static HasError(value) {
      if (!(value instanceof ReactiveValue)) {
        return false
      }
      return value.optionalGettable != null
        && value.optionalGettable.optionalLock != null
        && lockFirstError(value.optionalGettable.optionalLock) != null
    }

    static OptionalErrorMessage(value) {
      if (!(value instanceof ReactiveValue)) {
        return undefined
      }
      const optionalLock = value.optionalGettable == null ? undefined : value.optionalGettable.optionalLock
      const optionalError = optionalLock == null ? undefined : lockFirstError(optionalLock)
      return optionalError == null ? undefined : optionalError.message
    }

    static OptionalRetryFlag(value) {
      if (!(value instanceof ReactiveValue)) {
        return undefined
      }
      const optionalLock = value.optionalGettable == null ? undefined : value.optionalGettable.optionalLock
      const optionalError = optionalLock == null ? undefined : lockFirstError(optionalLock)
      const optionalRetryFlagId = optionalError == null ? undefined : optionalError.optionalRetryFlagId
      if (optionalRetryFlagId == null) {
        return undefined
      }
      return Reactive.Dereference(optionalRetryFlagId)
    }

    static HasRetryFlag(value) {
      return Reactive.OptionalRetryFlag(value) !== undefined
    }

    // Return value locked with a fresh error appended.
    static AddError(value, errorMessage, optionalRetryFlagId) {
      Security.Gettable(value)
      const reactiveValue = Reactive.Create(value)
      const newError = new ReactiveError(errorMessage, optionalRetryFlagId)
      const newLock = lockLeaf(new ReactiveLock(newError))
      const newGettable = new ReactiveGettable(newLock, reactiveValue.optionalGettable.value)
      return new ReactiveValue(newGettable, reactiveValue.optionalSettable)
    }

    // Conditionally lock a value (loading state) when locked is truthy.
    static Lock(value, locked) {
      Security.Gettable(value)
      Security.Gettable(locked)
      const nativeValue = value instanceof ReactiveValue ? (value.optionalGettable == null ? undefined : value.optionalGettable.value) : value
      const nativeLocked = locked instanceof ReactiveValue ? (locked.optionalGettable == null ? undefined : locked.optionalGettable.value) : locked
      const optionalLock = nativeLocked ? lockLeaf(new ReactiveLock(undefined)) : undefined
      const gettable = new ReactiveGettable(optionalLock, nativeValue)
      return new ReactiveValue(gettable, undefined)
    }

    // Return value with lockSource's lock merged in (error beats loading). Preserves
    // value's settability.
    static WithLockFrom(value, lockSource) {
      Security.Gettable(value)
      Security.Gettable(lockSource)

      const sourceLock = lockSource instanceof ReactiveValue && lockSource.optionalGettable != null
        ? lockSource.optionalGettable.optionalLock
        : undefined
      if (sourceLock == null) {
        return value
      }

      const reactiveValue = Reactive.Create(value)
      const existingLock = reactiveValue.optionalGettable == null ? undefined : reactiveValue.optionalGettable.optionalLock
      // Union the two lock trees (a direct error beats a direct loading lock, handled in lockMerge).
      const mergedLock = lockMerge(existingLock, sourceLock)

      const nativeValue = reactiveValue.optionalGettable.value
      const gettable = new ReactiveGettable(mergedLock, nativeValue)
      return new ReactiveValue(gettable, reactiveValue.optionalSettable)
    }

    // ---- private: updates split ----------------------------------------------

    // Updates targeting settables with NO reverse callback (plain state writes).
    static _getStateUpdates(updatesMap, programValues) {
      const result = {}
      for (const key of Object.keys(updatesMap)) {
        const update = updatesMap[key]
        const settableId = parseInt(key, 10)
        if (update.optionalSettable == null) {
          continue
        }
        const updateHasCallback = update.optionalSettable.optionalReverseCallback != null
        const originalValue = programValues[settableId]
        const originalHasCallback = originalValue != null && originalValue.optionalSettable != null
          && originalValue.optionalSettable.optionalReverseCallback != null
        if (!updateHasCallback && !originalHasCallback) {
          result[key] = update
        }
      }
      return result
    }

    // Updates targeting settables WITH a reverse callback (reverse triggers). If only
    // the original has the callback, rebuild the update carrying it.
    static _getReverseUpdates(updatesMap, programValues) {
      const result = {}
      for (const key of Object.keys(updatesMap)) {
        const update = updatesMap[key]
        const settableId = parseInt(key, 10)
        if (update.optionalSettable == null) {
          continue
        }
        const updateHasCallback = update.optionalSettable.optionalReverseCallback != null
        const originalValue = programValues[settableId]
        const originalSettable = originalValue == null ? null : originalValue.optionalSettable
        const originalHasCallback = originalSettable != null && originalSettable.optionalReverseCallback != null

        if (updateHasCallback) {
          result[key] = update
        } else if (originalHasCallback) {
          const settableWithCallback = new ReactiveSettable(settableId, originalSettable.optionalReverseCallback)
          result[key] = new ReactiveValue(update.optionalGettable, settableWithCallback)
        }
      }
      return result
    }

    static _mergeUpdates(destinationMap, stateUpdates) {
      const result = Object.assign({}, destinationMap)
      for (const key of Object.keys(stateUpdates)) {
        const update = stateUpdates[key]
        Security.Settable(update)
        result[key] = update
      }
      return result
    }

    // ---- private: reverse processes ------------------------------------------

    static _createReverseProcesses(reverseUpdates) {
      const newReverseProcesses = {}
      for (const key of Object.keys(reverseUpdates)) {
        const settableId = parseInt(key, 10)
        newReverseProcesses[settableId] = Reactive._createReverseProcess(settableId, reverseUpdates[key])
      }
      return newReverseProcesses
    }

    static _createReverseProcess(settableId, update) {
      Security.Reverse(update)
      const reverseCallback = update.optionalSettable.optionalReverseCallback
      const targetValue = update.optionalGettable.value
      const callback = () => reverseCallback(targetValue)
      return new ReactiveProcess(callback, [])
    }

    static _runReverseProcesses(reverseProcesses) {
      const unfinished = {}
      for (const key of Object.keys(reverseProcesses)) {
        const settableId = parseInt(key, 10)
        const optionalUnfinishedProcess = Reactive._runReverseProcess(settableId, reverseProcesses[key])
        if (optionalUnfinishedProcess != null) {
          unfinished[settableId] = optionalUnfinishedProcess
        }
      }
      return unfinished
    }

    // Run one reverse process: run its callback (which fans out Sets), collect ONLY
    // the State targets actually Set, combine their locks, and apply that lock to just
    // those targets. If locked, return the process as unfinished for the next run.
    static _runReverseProcess(settableId, process) {
      const previousDestinationValueMap = Reactive._destinationValueMap
      const previousReverseSetTargets = Reactive._reverseSetTargets
      const previousReverseSetPaths = Reactive._reverseSetPaths
      Reactive._destinationValueMap = {}
      Reactive._reverseSetTargets = []
      Reactive._reverseSetPaths = {}

      let setTargetIds = []
      let setTargetValues = []
      let reverseReadValues = []
      const previousReverseReadValues = Reactive._reverseReadValues
      const updatedScopeItems = Reactive._runCallback(process.rootScopeItems, () => {
        Reactive._reverseReadValues = []
        process.callback()
        reverseReadValues = Reactive._reverseReadValues
        Reactive._reverseReadValues = previousReverseReadValues
        setTargetIds = Reactive._reverseSetTargets.slice()
        for (const id of setTargetIds) {
          const value = Reactive._destinationValueMap[id]
          if (value != null) {
            setTargetValues.push(value)
          }
        }
      })

      const setPaths = Reactive._reverseSetPaths
      Object.assign(previousDestinationValueMap, Reactive._destinationValueMap)
      Reactive._destinationValueMap = previousDestinationValueMap
      Reactive._reverseSetTargets = previousReverseSetTargets
      Reactive._reverseSetPaths = previousReverseSetPaths

      if (setTargetIds.length === 0) {
        return undefined
      }

      // Unfinished iff a Set value is still locked (loading/error): the reverse cannot settle
      // and re-runs next cycle. While unfinished, the states it wrote are WRITE-locked so
      // nothing else changes them mid-flight; on the completing run the write lock lifts.
      let unfinished = false
      for (const value of setTargetValues) {
        if (value.optionalGettable != null && value.optionalGettable.optionalLock != null) {
          unfinished = true
        }
      }
      // A whole-value write lock for read-dependencies (a reverse reads a whole state leaf).
      const readDepWriteLock = unfinished ? lockLeaf(new ReactiveLock(undefined)) : undefined

      for (const id of setTargetIds) {
        const update = Reactive._destinationValueMap[id]
        if (update == null) {
          continue
        }
        const optionalExisting = Reactive._sourceValueMap[id]
        const existingHasGettable = optionalExisting != null && optionalExisting.optionalGettable != null
        const hasGettable = existingHasGettable || update.optionalGettable != null
        const nativeValue = update.optionalGettable != null
          ? update.optionalGettable.value
          : (existingHasGettable ? optionalExisting.optionalGettable.value : undefined)
        // READ lock: keep the target's OWN (already path-scoped) lock, so a field set to a
        // loading value spins on that field alone and its siblings stay live.
        const readTree = update.optionalGettable != null ? update.optionalGettable.optionalLock : undefined
        // WRITE lock: while unfinished, lock EXACTLY the field paths this reverse wrote
        // (root..leaf), so editing a sibling field of the same state stays enabled.
        const writeTree = unfinished ? Reactive._writeTreeForPaths(setPaths[id]) : undefined
        const optionalGettable = hasGettable ? new ReactiveGettable(readTree, nativeValue, writeTree) : undefined
        const settable = new ReactiveSettable(id, undefined)
        const valueToSet = new ReactiveValue(optionalGettable, settable)
        Reactive._sourceValueMap[id] = valueToSet
        Reactive._destinationValueMap[id] = valueToSet
      }

      // Write-lock the STABLE state leaves this reverse READ from its closure, so nothing
      // else changes its inputs while it runs (and the controls that set them disable).
      // Skips: its own Set targets (locked above); non-state reads; and states that are
      // themselves READ-locked (loading) - those are async inputs a driver must still fill,
      // so write-locking them would deadlock. The write lock is present while unfinished and
      // cleared (undefined) on the completing run, so it lifts when the process finishes.
      const seenReadIds = {}
      for (const readValue of reverseReadValues) {
        if (!(readValue instanceof ReactiveValue) || readValue.optionalSettable == null) {
          continue
        }
        if (readValue.optionalSettable.optionalReverseCallback != null) {
          continue
        }
        const readId = readValue.optionalSettable.id
        if (seenReadIds[readId] === true || setTargetIds.indexOf(readId) >= 0) {
          continue
        }
        seenReadIds[readId] = true
        const existingRead = Reactive._sourceValueMap[readId]
        if (existingRead == null || existingRead.optionalGettable == null || existingRead.optionalGettable.optionalLock != null) {
          continue
        }
        const readGettable = existingRead.optionalGettable
        const relockedGettable = new ReactiveGettable(readGettable.optionalLock, readGettable.value, readDepWriteLock)
        const relockedValue = new ReactiveValue(relockedGettable, new ReactiveSettable(readId, undefined))
        Reactive._sourceValueMap[readId] = relockedValue
        Reactive._destinationValueMap[readId] = relockedValue
      }

      if (!unfinished) {
        return undefined
      }
      return new ReactiveProcess(process.callback, updatedScopeItems)
    }

    // The WRITE-lock tree for the field paths a reverse wrote to one state: a lock at each
    // path (root..leaf), merged. No paths, or a whole-value ([]) set, locks the whole value.
    static _writeTreeForPaths(paths) {
      if (paths == null || paths.length === 0) {
        return lockLeaf(new ReactiveLock(undefined))
      }
      let tree = null
      for (const path of paths) {
        const leaf = lockScope(path, lockLeaf(new ReactiveLock(undefined)))
        tree = lockMerge(tree, leaf)
      }
      return tree
    }

    // ---- private: lock/error combination -------------------------------------

    static _getLocks(values) {
      const locks = []
      for (const value of values) {
        const optionalLock = value.optionalGettable == null ? undefined : value.optionalGettable.optionalLock
        if (optionalLock != null) {
          locks.push(optionalLock)
        }
      }
      return locks
    }

    static _getErrors(trees) {
      const errors = []
      for (const tree of trees) {
        lockAllErrors(tree, errors)
      }
      return errors
    }

    static _getRetryFlagIds(errors) {
      const retryFlagIds = []
      for (const error of errors) {
        if (error.optionalRetryFlagId != null) {
          retryFlagIds.push(error.optionalRetryFlagId)
        }
      }
      return retryFlagIds
    }

    // A combined retry flag that, when Set, triggers every upstream retry flag.
    static _generateCombinedRetryFlagId(retryFlagIds) {
      const retryId = Reactive.StableId()
      const retrySettable = new ReactiveSettable(retryId, (newValue) => {
        for (const flagId of retryFlagIds) {
          const flag = Reactive.Dereference(flagId)
          Reactive.Set(flag, true)
        }
      })
      const combinedRetryFlag = new ReactiveValue(new ReactiveGettable(undefined, false), retrySettable)
      Reactive._destinationValueMap[retryId] = combinedRetryFlag
      return retryId
    }

    // ---- private: scope machinery ---------------------------------------------

    static _nextSourceScopeItem() {
      const sourceScopeItems = Reactive._sourceScopeItems
      const destinationScopeItems = Reactive._destinationScopeItems
      const index = destinationScopeItems.length
      if (index < sourceScopeItems.length) {
        return sourceScopeItems[index]
      }
      const id = Reactive._nextIdCounter++
      return new ReactiveScopeItem(id, {})
    }

    static _runCallback(sourceScopeItems, callback) {
      const previousSourceScopeItems = Reactive._sourceScopeItems
      const previousDestinationScopeItems = Reactive._destinationScopeItems
      Reactive._sourceScopeItems = sourceScopeItems
      Reactive._destinationScopeItems = []

      Reactive._lastCallbackResult = callback()

      const updatedScopeItems = Reactive._destinationScopeItems
      Reactive._sourceScopeItems = previousSourceScopeItems
      Reactive._destinationScopeItems = previousDestinationScopeItems
      return updatedScopeItems
    }
  }

Section 3 - reification registry (net-new)

  // ==========================================================================
  // Section 3 - reification registry (net-new)
  // ==========================================================================
  //
  // Module/Function/Type/Union/Field/Variant build a tree of real JS objects on the
  // global scope so authored code can write Content.Text(...), Clip.Param.Payload.Union,
  // Input.Dropdown.Option(...) directly. Modules MERGE across files (a namespace may be
  // declared in many files). A Function is also a namespace object (a pseudo-module),
  // so Input.Dropdown can be a function AND the home of Input.Dropdown.Option. A Type is a
  // descriptor object that is ALSO a namespace node (types can host static functions).

  // The container currently open for Module/Function/Type/Union declarations. The root
  // is the global object, so top-level modules become global names.
  const globalScope = typeof globalThis !== 'undefined' ? globalThis : this
  const namespaceStack = [globalScope]
  // The type descriptor currently open for Field/Variant declarations.
  const typeStack = []
  // The dotted module path currently being DECLARED (load time), so a function can
  // record which module it belongs to. Channel names are scoped to that module.
  const modulePathStack = []
  // The module path of the function currently EXECUTING, and the stack of executing
  // functions (for registering nested pseudo-module functions on the parent for the
  // duration of its call). Both are runtime, driven by the Function wrapper.
  const runtimeModuleStack = []
  // Containers (a function object or a runtime module object) that nested runtime
  // declarations register ON. Top = where to attach the next nested Module/Function.
  const runtimeContainerStack = []
  // The stack of executing FUNCTIONS; each carries the restorations that undo the
  // nested declarations it introduced, run when that function returns.
  const runtimeFunctionStack = []
  // The stack of VALUE-ARGUMENT frames, one per executing function: { values, index }.
  // Reactive.Argument pulls the next value off the top frame (positional, type-checked).
  const argumentsStack = []
  // Registered drivers: qualified channel name -> factory(params, addUpdates).
  const driverRegistry = {}
  // Channels whose driver is ALWAYS active (no Mount config needed) - core paramless drivers
  // like 'calculate' that every program relies on. Mount activates these automatically.
  const alwaysActiveDrivers = {}

  // ---- interfaces / modes / With: the interface-binding environment (doc 14) ----
  //
  // An interface (Reactive.Interface) is an abstract shape; an implementation is a namespace
  // that provides its members; Reactive.With binds interface -> implementation for a scope a
  // Reactive.Mode establishes. A member CALL dispatches to the currently bound implementation.
  //
  // Two environments, deliberately asymmetric (doc 14 sec 4):
  //  - userAmbientStack: the environments USER CODE and SLOTS resolve against. Reactive.Mode
  //    pushes a fresh one (empty = all interfaces reset); Reactive.With fills it; the base
  //    env (index 0) holds the root defaults. Each env keeps __parent for pass-through.
  //  - resolveAmbient: the environment interface calls resolve against RIGHT NOW. It is the
  //    mode env while user code runs, and an implementation's captured home while its body
  //    runs. A Reactive.Function snapshots resolveAmbient as its __home at definition; the
  //    dispatcher runs a member under that home; a Slot runs the caller's builder back under
  //    the user ambient (so the mode retargets user code, implementations run at home).
  const baseAmbient = { __parent: null }
  const userAmbientStack = [baseAmbient]
  let resolveAmbient = baseAmbient
  let nextInterfaceId = 1
  // While declaring inside a Reactive.Interface body: { root, path } (root interface object +
  // the member path being built), so a nested Module extends the path and a Function becomes
  // a path-aware dispatcher instead of a runnable body.
  const interfaceStack = []
  // While RECORDING an interface member's signature: { args, returns }. An interface member's
  // body is run in this mode so Reactive.Argument / Reactive.Slot / Reactive.Returns record the
  // signature (for conformance checking + the future static pass) instead of pulling values.
  const signatureRecordingStack = []

  // Look up the implementation bound for an interface in the current resolveAmbient. A Mode's
  // env is a clean slate (the seal), so this does NOT chain to __parent - only explicit With
  // (including pass-through, which copies the parent binding in) populates an env. Returns
  // null when unbound.
  function resolveImplementation(interfaceId) {
    const env = resolveAmbient
    return (env != null && Object.prototype.hasOwnProperty.call(env, interfaceId)) ? env[interfaceId] : null
  }

  // Conformance: the interface member paths an implementation does NOT provide as a callable
  // (empty = fully conformant). Used to check an implementation at Reactive.With time so a gap
  // is reported at the bind site, not lazily when the missing member is first dispatched.
  function implementationGaps(interfaceObject, implementation) {
    const members = interfaceObject.__interfaceMembers == null ? [] : interfaceObject.__interfaceMembers
    const missing = []
    for (let i = 0; i < members.length; i++) {
      const path = members[i].path
      let target = implementation
      for (let j = 0; j < path.length; j++) {
        target = target == null ? null : target[path[j]]
      }
      if (typeof target !== 'function') {
        missing.push(path.join('.'))
      }
    }
    return missing
  }

  // A path-aware dispatcher for one interface member. Calling it forwards to the current
  // implementation's same member, run under that member's home (doc 14 sec 2/4).
  function makeInterfaceDispatcher(rootInterface, memberPath) {
    return function () {
      const impl = resolveImplementation(rootInterface.__interfaceId)
      if (impl == null) {
        throw new Error('Reactive: no implementation of interface "' + rootInterface.__interfaceName + '" in scope (member .' + memberPath.join('.') + '). Bind it with Reactive.With inside a Reactive.Mode.')
      }
      let target = impl
      for (let i = 0; i < memberPath.length; i++) {
        target = target == null ? null : target[memberPath[i]]
      }
      if (typeof target !== 'function') {
        throw new Error('Reactive: implementation of "' + rootInterface.__interfaceName + '" is missing member .' + memberPath.join('.'))
      }
      const saved = resolveAmbient
      resolveAmbient = target.__home != null ? target.__home : saved
      try {
        return target.apply(null, arguments)
      } finally {
        resolveAmbient = saved
      }
    }
  }

  function currentContainer() {
    return namespaceStack[namespaceStack.length - 1]
  }

  // Register `value` on `container[name]` for the duration of the nearest enclosing
  // function call, restoring the previous binding when that function returns. This is
  // how a pseudo-module declaration (an inner Function or Module) exists only while its
  // defining function runs, and re-entrantly.
  function registerTemporarily(container, name, value) {
    const had = Object.prototype.hasOwnProperty.call(container, name)
    const previous = had ? container[name] : undefined
    container[name] = value
    const entry = runtimeFunctionStack[runtimeFunctionStack.length - 1]
    entry.restorations.push(() => {
      if (had) {
        container[name] = previous
      } else {
        delete container[name]
      }
    })
  }

  function currentModulePath() {
    return modulePathStack.join('.')
  }

  // Qualify a short channel name with the module of the currently executing function,
  // so 'content' inside a Dom function is 'Dom.content' - channels are module-scoped
  // and cannot collide across modules.
  // Channels are FRAME-SCOPED bare labels (doc 14 sec 6): a channel name is used as-is, and
  // routing is by the emission-frame stack - an emission is caught by the nearest enclosing
  // Gather of that name, or, if none, by the root Driver of that name. No module prefix, so a
  // Gather in one module catches an Emit in another (e.g. a Region gathering 'items' catches a
  // sub-module implementation's 'items'). Kept as a function so the one call site is obvious.
  function qualifyChannel(name) {
    return name
  }

  function currentType() {
    if (typeStack.length === 0) {
      throw new Error('Reactive.Field/Variant called outside a Reactive.Type/Union body')
    }
    return typeStack[typeStack.length - 1]
  }

  // A type descriptor is stashed on a namespace object under this key, so the object
  // doubles as a usable type reference and a namespace node.
  const TYPE_KEY = '__reactiveType'

  Reactive.Module = function (name, body) {
    if (runtimeFunctionStack.length > 0) {
      // Runtime (nested) module - e.g. Dom.Event declared inside the Dom scope's body.
      // A fresh namespace registered on the current container for the enclosing
      // function's call; its inner functions attach to it. It carries no module path
      // (channel scoping uses the enclosing function's module).
      const container = runtimeContainerStack[runtimeContainerStack.length - 1]
      const moduleObject = {}
      registerTemporarily(container, name, moduleObject)
      runtimeContainerStack.push(moduleObject)
      try {
        body()
      } finally {
        runtimeContainerStack.pop()
      }
      return moduleObject
    }

    // Load-time module: register on the namespace permanently and track the path.
    const container = currentContainer()
    const existing = container[name]
    const namespaceObject = existing != null ? existing : {}
    container[name] = namespaceObject
    namespaceStack.push(namespaceObject)
    modulePathStack.push(name)
    // Inside a Reactive.Interface body, a nested module extends the member path so its
    // functions dispatch as <interface>.<...>.<member> (e.g. Maths.Integer.Add).
    const inInterface = interfaceStack.length > 0
    if (inInterface) {
      interfaceStack[interfaceStack.length - 1].path.push(name)
    }
    try {
      body()
    } finally {
      if (inInterface) {
        interfaceStack[interfaceStack.length - 1].path.pop()
      }
      modulePathStack.pop()
      namespaceStack.pop()
    }
    return namespaceObject
  }

  // A function is callable AND a namespace (a pseudo-module). The definition closure's
  // PARAMETERS are the function's GENERIC type parameters (arity = number of generics);
  // VALUE arguments are declared inside the body with Reactive.Argument (positional,
  // typed). So a non-generic function (arity 0) is called Fn(...values); a generic one
  // Fn(...types)(...values) - curried, explicit, no inference. Each body run sits in its
  // own emission frame + arguments frame and records its module (for channel scoping); a
  // Reactive.Function declared INSIDE a running body is a nested pseudo-module function,
  // registered on the enclosing function for that call only (re-entrant), capturing the
  // enclosing call's locals by closure - the sole legal place to Emit into its Gather.
  Reactive.Function = function (name, impl) {
    // Declared inside a Reactive.Interface body: a SIGNATURE, realised as a path-aware
    // DISPATCHER. Calling it looks up the current implementation of the root interface and
    // forwards to the same member, run under that member's home (doc 14 sec 2/4). The impl
    // closure is kept for the future static pass but is NOT run at dispatch.
    if (interfaceStack.length > 0) {
      const frame = interfaceStack[interfaceStack.length - 1]
      const memberPath = frame.path.concat([name])
      // Record the member's signature by running its closure in signature mode (Argument /
      // Slot / Returns record instead of pulling). Signature closures are pure declarations.
      const signature = { args: [], returns: undefined }
      signatureRecordingStack.push(signature)
      try {
        impl()
      } finally {
        signatureRecordingStack.pop()
      }
      const dispatcher = makeInterfaceDispatcher(frame.root, memberPath)
      dispatcher.__reactiveSignature = signature
      if (frame.root.__interfaceMembers == null) {
        frame.root.__interfaceMembers = []
      }
      frame.root.__interfaceMembers.push({ path: memberPath, signature: signature })
      currentContainer()[name] = dispatcher
      return dispatcher
    }

    const runtime = runtimeFunctionStack.length > 0
    const definitionModulePath = runtime
      ? (runtimeModuleStack.length > 0 ? runtimeModuleStack[runtimeModuleStack.length - 1] : '')
      : currentModulePath()
    const genericArity = impl.length

    function runBody(typeArgs, valueArgs) {
      // Type-check mode: branch coverage forces recursive branches that real data would stop, so
      // bound the traversal by DEPTH (not function identity - scope functions like Dom re-enter
      // themselves legitimately for every nested tag). An over-deep call returns a permissive
      // placeholder so nothing crashes and the pass terminates.
      if (Reactive._typeChecking && Reactive._typeCheckDepth >= Reactive._typeCheckDepthLimit) {
        return typeCheckPlaceholder
      }
      if (Reactive._typeChecking) {
        Reactive._typeCheckDepth++
      }
      Reactive._emissionFrameStack.push({})
      runtimeModuleStack.push(fn.__modulePath)
      runtimeContainerStack.push(fn)
      const functionEntry = { restorations: [] }
      runtimeFunctionStack.push(functionEntry)
      argumentsStack.push({ values: valueArgs, index: 0 })
      try {
        const result = impl.apply(null, typeArgs)
        // Enforce a declared return type (Reactive.Returns in the body). A result already
        // carrying its type is trusted; anything else is validated against the return type.
        if (functionEntry.returnType != null && result != null && result[TYPE_SYMBOL] == null) {
          const returnDescriptor = Reactive.DescriptorOf(functionEntry.returnType)
          validateAgainstDescriptor(result, returnDescriptor, (fn.__modulePath === '' ? '' : fn.__modulePath + '.') + name + ' return')
        }
        return result
      } finally {
        if (Reactive._typeChecking) {
          Reactive._typeCheckDepth--
        }
        argumentsStack.pop()
        runtimeFunctionStack.pop()
        for (let i = functionEntry.restorations.length - 1; i >= 0; i--) {
          functionEntry.restorations[i]()
        }
        runtimeContainerStack.pop()
        runtimeModuleStack.pop()
        const frame = Reactive._emissionFrameStack.pop()
        // Bubble ungathered channels to the LEXICAL parent frame (closure semantics), or to the
        // root frame for a top-level function (drivers). NOT the dynamic caller.
        const rootFrame = Reactive._emissionFrameStack.length > 0 ? Reactive._emissionFrameStack[0] : null
        const parent = fn.__lexicalParentFrame != null ? fn.__lexicalParentFrame : rootFrame
        if (parent != null && parent !== frame) {
          for (const channel of Object.keys(frame)) {
            if (frame[channel].length > 0) {
              parent[channel] = (parent[channel] == null ? [] : parent[channel]).concat(frame[channel])
            }
          }
        }
      }
    }

    const fn = function () {
      if (genericArity === 0) {
        return runBody([], Array.prototype.slice.call(arguments))
      }
      const typeArgs = Array.prototype.slice.call(arguments)
      return function () {
        return runBody(typeArgs, Array.prototype.slice.call(arguments))
      }
    }

    fn.__reactiveImpl = impl
    fn.__modulePath = definitionModulePath
    fn.__genericArity = genericArity
    // Home = the interface environment visible where this function is DEFINED. An
    // implementation resolves its own interface calls against this, so it reaches the layer
    // below the mode it is later installed into (doc 14 sec 4).
    fn.__home = resolveAmbient
    // Lexical parent frame (channels are closures, doc 14 sec 6): the emission frame of the
    // activation this function is DEFINED inside (its enclosing pseudo-module). Its emissions
    // bubble to THIS captured frame - the exact one from its defining context, not the nearest
    // gather on the dynamic call stack. A top-level function has no defining activation, so it
    // bubbles to the root (where drivers gather).
    fn.__lexicalParentFrame = runtime ? Reactive._emissionFrameStack[Reactive._emissionFrameStack.length - 1] : null

    if (runtime) {
      const container = runtimeContainerStack[runtimeContainerStack.length - 1]
      registerTemporarily(container, name, fn)
      return fn
    }

    const container = currentContainer()
    const existing = container[name]
    if (existing != null && typeof existing === 'object') {
      Object.assign(fn, existing)
    }
    container[name] = fn
    return fn
  }

  // Reactive.Argument(type) - declare and pull the next VALUE argument of the executing
  // function, in order, type-checked. `type` may be a reified type, a generic type
  // instantiated inline (Collection.Array(ElementType)), a Reactive.Callback signature,
  // or Reactive.Any. Returns the value (a plain native, a binding, or a callback closure).
  Reactive.Argument = function (type) {
    if (signatureRecordingStack.length > 0) {
      signatureRecordingStack[signatureRecordingStack.length - 1].args.push({ kind: 'value', type: type })
      return undefined
    }
    if (argumentsStack.length === 0) {
      throw new Error('Reactive.Argument called outside a reactive function body')
    }
    const frame = argumentsStack[argumentsStack.length - 1]
    const value = frame.values[frame.index]
    frame.index += 1

    if (type != null && type.__reactiveCallback === true) {
      if (typeof value !== 'function') {
        throw new Error('Reactive.Argument expected a callback (function)')
      }
      return value
    }
    const descriptor = Reactive.DescriptorOf(type)
    // Full structural validation (records, unions, arrays, primitives, deep). A value already
    // carrying its type (a typed State, a dotted sub-binding) was validated at its source, so
    // it is trusted; anything else (a plain native, a raw binding, a Calculate result) is
    // validated here against the declared type, throwing a located Type error on a mismatch.
    if (descriptor != null && value != null && value[TYPE_SYMBOL] == null) {
      validateAgainstDescriptor(value, descriptor, descriptor.name)
    }
    // Apply the declared type: a record/union argument becomes a typed proxy so its fields
    // and union options dot. A value arriving already typed (a typed State, a sub-binding
    // dotted out of a parent) carries TYPE_SYMBOL and is returned untouched; a raw binding
    // (e.g. an element handed in by Collection.Array.Each) is typed here.
    if (descriptor != null && (descriptor.kind === 'record' || descriptor.kind === 'union')) {
      if (value != null && value[TYPE_SYMBOL] == null) {
        return Reactive._typed(value, descriptor)
      }
    }
    return value
  }

  // Reactive.Callback(optionalReturnType, optionalInputsClosure) - a callback parameter's
  // signature: its return type and a closure that declares its inputs via Reactive.Argument
  // (used by the static pass; at runtime a callback is just invoked). Bare Reactive.Callback()
  // is a void, no-input builder (the common component case: `contents`, `content`, `body`).
  Reactive.Callback = function (optionalReturnType, optionalInputsClosure) {
    return { __reactiveCallback: true, returnType: optionalReturnType, inputsClosure: optionalInputsClosure }
  }

  // Invoke a callback with explicit values bound as its Reactive.Argument inputs. A plain
  // callback has no arguments frame of its own, so we push one for the call (used by
  // Collection.Optional.If to hand a payload to its branch callback).
  function invokeCallbackWithArguments(callback, values) {
    argumentsStack.push({ values: values, index: 0 })
    try {
      return callback()
    } finally {
      argumentsStack.pop()
    }
  }

Interfaces / modes / With (doc 14)

  // ==========================================================================
  // Interfaces / modes / With (doc 14)
  // ==========================================================================

  // Declare an abstract interface: typed member signatures (a Reactive.Function inside becomes
  // a dispatcher, not a runnable body) and abstract types, with no implementation. Like a
  // Module in how it builds a namespace, but its functions dispatch to the currently bound
  // implementation of this interface.
  Reactive.Interface = function (name, body) {
    const container = currentContainer()
    const existing = container[name]
    const interfaceObject = (existing != null && typeof existing === 'object') ? existing : {}
    if (interfaceObject.__interfaceId == null) {
      interfaceObject.__interfaceId = 'interface#' + (nextInterfaceId++)
    }
    interfaceObject.__interfaceName = currentModulePath() === '' ? name : currentModulePath() + '.' + name
    container[name] = interfaceObject
    namespaceStack.push(interfaceObject)
    modulePathStack.push(name)
    interfaceStack.push({ root: interfaceObject, path: [] })
    try {
      body()
    } finally {
      interfaceStack.pop()
      modulePathStack.pop()
      namespaceStack.pop()
    }
    return interfaceObject
  }

  // Reactive.Returns(type) - declare a member's return type inside its signature body (where
  // generic type parameters are in scope). Records into the signature while an interface member
  // is being recorded; otherwise a no-op (metadata for the static pass).
  Reactive.Returns = function (type) {
    if (signatureRecordingStack.length > 0) {
      signatureRecordingStack[signatureRecordingStack.length - 1].returns = type
      return
    }
    // In a running body: record the declared return type so runBody validates the result.
    if (runtimeFunctionStack.length > 0) {
      runtimeFunctionStack[runtimeFunctionStack.length - 1].returnType = type
    }
  }

  // Reactive.Mode(body) - enter a mode. Reset all interface bindings (the seal), run body (a
  // run of Reactive.With statements then the code that uses them), restore on exit. User code
  // inside resolves interface calls against the mode's bindings.
  Reactive.Mode = function (body) {
    const parent = userAmbientStack[userAmbientStack.length - 1]
    const env = { __parent: parent }
    userAmbientStack.push(env)
    const saved = resolveAmbient
    resolveAmbient = env
    try {
      return body()
    } finally {
      resolveAmbient = saved
      userAmbientStack.pop()
    }
  }

  // Reactive.With(interface, implementation) - bind an interface to an implementation in the
  // current mode (or the base environment, outside any mode). Reactive.With(interface) with no
  // implementation is PASS-THROUGH: copy the implementation from the enclosing scope so it
  // shines through the mode's reset.
  Reactive.With = function (interfaceObject, implementation) {
    if (interfaceObject == null || interfaceObject.__interfaceId == null) {
      throw new Error('Reactive.With: first argument is not a Reactive.Interface')
    }
    const env = userAmbientStack[userAmbientStack.length - 1]
    const id = interfaceObject.__interfaceId
    if (arguments.length >= 2 && implementation !== undefined) {
      const gaps = implementationGaps(interfaceObject, implementation)
      if (gaps.length > 0) {
        throw new Error('Reactive.With(' + interfaceObject.__interfaceName + '): implementation does not conform - missing member(s): ' + gaps.join(', '))
      }
      env[id] = implementation
      return
    }
    let ancestor = env.__parent
    while (ancestor != null && !Object.prototype.hasOwnProperty.call(ancestor, id)) {
      ancestor = ancestor.__parent
    }
    if (ancestor == null) {
      throw new Error('Reactive.With(' + interfaceObject.__interfaceName + '): pass-through, but no implementation is bound in the enclosing scope')
    }
    env[id] = ancestor[id]
  }

  // Reactive.Conforms(interface, implementation) - the interface member paths the implementation
  // does not provide (empty array = conformant). The conformance/static check, exposed for
  // tooling; Reactive.With calls it and throws on a gap.
  Reactive.Conforms = function (interfaceObject, implementation) {
    if (interfaceObject == null || interfaceObject.__interfaceId == null) {
      throw new Error('Reactive.Conforms: first argument is not a Reactive.Interface')
    }
    return implementationGaps(interfaceObject, implementation)
  }

  // Reactive.Signatures(interface) - the recorded member signatures of an interface: a list of
  // { path, signature:{ args:[{kind,type|returnType}], returns } }. For tooling / the static pass.
  Reactive.Signatures = function (interfaceObject) {
    if (interfaceObject == null || interfaceObject.__interfaceId == null) {
      throw new Error('Reactive.Signatures: first argument is not a Reactive.Interface')
    }
    return interfaceObject.__interfaceMembers == null ? [] : interfaceObject.__interfaceMembers
  }

  // Reactive.Slot(returnType, optionalSignature) - declare a type-safe CALLBACK argument: the
  // next value argument is the caller's callback, and `optionalSignature` is a closure that
  // declares the callback's own inputs (Reactive.Argument / nested Reactive.Slot) and, with
  // returnType, its shape. Returns a HANDLE the provider invokes with the callback's argument
  // values: handle(v1, v2, ...). Invoking it (doc 14 sec 4/5):
  //   - runs the callback under the CURRENT mode (the user ambient at invoke time, not a
  //     captured one), so its interface calls hit the mode's implementations while the
  //     enclosing implementation keeps running at its home;
  //   - supplies the values BOTH as an arguments frame (so a callback that pulls them with
  //     Reactive.Argument gets them, the framework convention) AND as JS arguments (so an
  //     interface member / dispatcher passed point-free forwards them to its implementation).
  // returnType / optionalSignature are metadata for the static pass; not enforced at runtime.
  Reactive.Slot = function (returnType, optionalSignature) {
    if (signatureRecordingStack.length > 0) {
      signatureRecordingStack[signatureRecordingStack.length - 1].args.push({ kind: 'slot', returnType: returnType, signature: optionalSignature })
      return function () { return undefined }
    }
    let provided
    if (argumentsStack.length > 0) {
      const frame = argumentsStack[argumentsStack.length - 1]
      provided = frame.values[frame.index]
      frame.index += 1
    }
    const callback = (typeof provided === 'function') ? provided : null
    const handle = function () {
      if (callback == null) {
        return undefined
      }
      const values = Array.prototype.slice.call(arguments)
      const saved = resolveAmbient
      resolveAmbient = userAmbientStack[userAmbientStack.length - 1]
      argumentsStack.push({ values: values, index: 0 })
      try {
        return callback.apply(null, values)
      } finally {
        argumentsStack.pop()
        resolveAmbient = saved
      }
    }
    handle.__slotReturnType = returnType
    handle.__slotSignature = optionalSignature
    return handle
  }

  function defineType(kind, name, body) {
    const container = currentContainer()
    const existing = container[name]
    const descriptor = (existing != null && existing[TYPE_KEY] != null) ? existing[TYPE_KEY] : { kind: kind, name: name, fields: [] }
    descriptor.kind = kind

    // A RECORD type is ALSO its positional constructor: called with the field values in
    // declaration order it returns a plain native object { field: value, ... } - no ids, no
    // reactive wrapping (settability is added later by whatever holds it). So its carrier is
    // a function; the constructor reads descriptor.fields at CALL time, so fields declared by
    // body() below are all present. Unions and primitives are not positionally constructable,
    // so their carrier stays a plain object. An already-created carrier (a re-opened type, or
    // a primitive being given a Native body) is reused as-is.
    let typeObject
    if (existing != null) {
      typeObject = existing
    } else if (kind === 'record') {
      typeObject = function () {
        const native = {}
        for (let i = 0; i < descriptor.fields.length; i++) {
          native[descriptor.fields[i].name] = arguments[i]
        }
        return native
      }
    } else {
      typeObject = {}
    }
    typeObject[TYPE_KEY] = descriptor
    container[name] = typeObject

    namespaceStack.push(typeObject)
    modulePathStack.push(name)
    typeStack.push(descriptor)
    try {
      body()
    } finally {
      typeStack.pop()
      modulePathStack.pop()
      namespaceStack.pop()
    }
    return typeObject
  }

  // A record type: a fixed set of named fields.
  Reactive.Type = function (name, body) {
    return defineType('record', name, body)
  }

  // A tagged union: all-optional fields where exactly one is present; the present
  // field name is the tag.
  Reactive.Union = function (name, body) {
    return defineType('union', name, body)
  }

  Reactive.Field = function (name, typeRef) {
    currentType().fields.push({ name: name, tag: undefined, type: typeRef })
  }

  Reactive.Variant = function (tag, typeRef) {
    currentType().fields.push({ name: tag, tag: tag, type: typeRef })
  }

  // Read a type descriptor off a reified type reference (or null for a non-type).
  Reactive.DescriptorOf = function (typeRef) {
    // A type carrier is a plain object (union/primitive) OR a function (a record, which is
    // also its positional constructor), so accept both; anything without TYPE_KEY is not a
    // reified type (the JS global Boolean, a Collection.Array carrier, a plain value).
    if (typeRef == null || (typeof typeRef !== 'object' && typeof typeRef !== 'function')) {
      return null
    }
    return typeRef[TYPE_KEY] == null ? null : typeRef[TYPE_KEY]
  }

  // ---- primitives -----------------------------------------------------------
  //
  // Leaf type descriptors referenced bare as field types. Text and Maths.Real/Integer
  // are predefined here (Maths.js notes Real/Integer are framework primitives; the
  // Text module in Text.js MERGES its functions onto this Text primitive). There is NO
  // Boolean primitive: a boolean is modelled as the tagged union Logic.Maybe, owned by
  // the Logic module (not the core), so the JS global `Boolean` is never shadowed.

  function definePrimitive(container, name) {
    const existing = container[name]
    const primitiveObject = existing != null ? existing : {}
    if (primitiveObject[TYPE_KEY] == null) {
      primitiveObject[TYPE_KEY] = { kind: 'primitive', name: name, fields: [] }
    }
    container[name] = primitiveObject
    return primitiveObject
  }

  definePrimitive(globalScope, 'Text')
  Reactive.Module('Maths', () => {
    const real = definePrimitive(currentContainer(), 'Real')
    real[TYPE_KEY].nativeValidator = function (v) {
      if (typeof v !== 'number' || Number.isNaN(v)) {
        throw new Error('Maths.Real must be a number, got ' + describeValue(v))
      }
      return v
    }
    const integer = definePrimitive(currentContainer(), 'Integer')
    integer[TYPE_KEY].nativeValidator = function (v) {
      if (typeof v !== 'number' || !Number.isInteger(v)) {
        throw new Error('Maths.Integer must be an integer, got ' + describeValue(v))
      }
      return v
    }
  })
  // A permissive leaf type: any value, no validation. For loosely-typed arguments (an
  // attribute value, a settable of unspecified type) until they are given precise types.
  Reactive.Any = definePrimitive({}, 'Any')

  // A native type is a primitive/leaf whose values are plain natives validated (and
  // optionally coerced) by a function. Called inside a Reactive.Type body (see Text.js:
  // Reactive.Type("Text", () => Reactive.Native(text => ...))). Native FUNCTION impls
  // are NOT written with this - a native function's body just uses Reactive.Calculate
  // to derive its result (e.g. Text.Equal).
  Reactive.Native = function (nativeValidator) {
    if (typeStack.length === 0) {
      throw new Error('Reactive.Native must be called inside a Reactive.Type/Union body')
    }
    const descriptor = currentType()
    descriptor.kind = 'primitive'
    descriptor.nativeValidator = nativeValidator
  }

Section 4 - strict type validation

  // ==========================================================================
  // Section 4 - strict type validation
  // ==========================================================================
  //
  // A reified descriptor is enforced when a value ENTERS the system (a typed State,
  // or a Set on a typed binding). Primitives run their Reactive.Native validator;
  // records check each field; unions check exactly one variant is present. Null /
  // undefined field values pass (nullability is not yet carried on descriptors), so
  // optional* fields may be absent. Nested validation is one pass over the tree.

  // Private per-binding tag holding the binding's type descriptor, so a Set can find
  // the type even for a leaf binding passed far from where it was typed. A Symbol so
  // it never collides with a data field and is invisible to the program.
  const TYPE_SYMBOL = Symbol('reactiveType')

  // A permissive stand-in returned by an over-deep call during the type-check pass. Any property
  // read yields an empty array, so a caller that expected a gathered array (e.g. Dom.Tag reading
  // child.content / child.attributes) keeps working; calling it yields an empty array too.
  const typeCheckPlaceholder = new Proxy(function () {}, {
    get: function () { return [] },
    apply: function () { return [] },
  })

  // Strict typing: on by default. A single switch so a production build can compile the
  // checks out, or a debugging session can turn them off. When off, validateAgainstDescriptor
  // is a pass-through.
  let strictTypes = true
  Reactive.StrictTypes = function (on) { strictTypes = on !== false }

  // Validate `value` against a reified type descriptor, deeply: primitives (native validator),
  // records (each present field), unions (exactly one variant, valid), arrays (each element vs
  // the element type). Handles a value that is a ReactiveValue - it validates the native inside,
  // but SKIPS a locked/loading value (its native is a placeholder) and a reverse sink (no
  // gettable). Throws a Type error naming the path on a mismatch. Returns `value` unchanged.
  function validateAgainstDescriptor(value, descriptor, path) {
    if (descriptor == null || !strictTypes) {
      return value
    }
    const here = path == null ? (descriptor.name || 'value') : path

    // Reach the native behind a reactive value; a locked/loading value is skipped.
    let native = value
    if (value instanceof ReactiveValue) {
      if (value.optionalGettable == null || value.optionalGettable.optionalLock != null) {
        return value
      }
      native = value.optionalGettable.value
    }

    if (descriptor.kind === 'primitive') {
      if (descriptor.nativeValidator != null && native != null) {
        try {
          descriptor.nativeValidator(native)
        } catch (e) {
          throw new Error('Type error at ' + here + ': ' + e.message)
        }
      }
      return value
    }

    if (descriptor.kind === 'array') {
      if (native == null || native.elements == null) {
        throw new Error('Type error at ' + here + ': expected an array of ' + describeType(descriptor.elementType))
      }
      const elementDescriptor = Reactive.DescriptorOf(descriptor.elementType)
      for (let i = 0; i < native.elements.length; i++) {
        validateAgainstDescriptor(native.elements[i].value, elementDescriptor, here + '[' + i + ']')
      }
      return value
    }

    if (descriptor.kind === 'record') {
      if (native == null || typeof native !== 'object') {
        throw new Error('Type error at ' + here + ': expected a ' + descriptor.name + ' record object, got ' + describeValue(native))
      }
      for (const field of descriptor.fields) {
        const fieldValue = native[field.name]
        if (fieldValue !== undefined && fieldValue !== null) {
          const fieldDescriptor = Reactive.DescriptorOf(field.type)
          validateAgainstDescriptor(fieldValue, fieldDescriptor, here + '.' + field.name)
        }
      }
      return value
    }

    if (descriptor.kind === 'union') {
      if (native == null || typeof native !== 'object') {
        throw new Error('Type error at ' + here + ': expected a ' + descriptor.name + ' union object, got ' + describeValue(native))
      }
      const presentTags = []
      for (const field of descriptor.fields) {
        if (native[field.name] !== undefined && native[field.name] !== null) {
          presentTags.push(field.name)
        }
      }
      if (presentTags.length !== 1) {
        throw new Error('Type error at ' + here + ': a ' + descriptor.name + ' union needs exactly one variant, found [' + presentTags.join(', ') + ']')
      }
      const tag = presentTags[0]
      const variant = descriptorField(descriptor, tag)
      const variantDescriptor = Reactive.DescriptorOf(variant.type)
      validateAgainstDescriptor(native[tag], variantDescriptor, here + '.' + tag)
      return value
    }

    return value
  }

  // Small helpers for readable Type error messages.
  function describeType(type) {
    const d = Reactive.DescriptorOf(type)
    return d != null && d.name != null ? d.name : 'value'
  }
  function describeValue(v) {
    if (v === null) return 'null'
    if (v === undefined) return 'undefined'
    if (typeof v === 'string') return 'a string'
    if (typeof v === 'number') return 'a number'
    if (typeof v === 'boolean') return 'a boolean'
    if (typeof v === 'function') return 'a function'
    return 'a ' + typeof v
  }

Section 5 - typed reversible accessors (the Proxy)

  // ==========================================================================
  // Section 5 - typed reversible accessors (the Proxy)
  // ==========================================================================
  //
  // Reactive._typed(binding, descriptor) is PRIVATE (never called by application or library
  // code, only by the engine: _makeState, Reactive.Argument, and the Proxy's own recursion).
  // It wraps a binding so dotted paths (param.payload.vec3.x) stay two-way. A RECORD/UNION descriptor
  // returns a Proxy over the (real) ReactiveValue: it still IS a ReactiveValue for the
  // engine, but a field/variant access returns a fresh reversible sub-binding built
  // with Reactive._objectField. A PRIMITIVE (leaf) needs no dotted access, so it is
  // returned unproxied, only tagged with its descriptor for Set validation. The native
  // data inside a binding is always PLAIN (no ids, no wrappers) - the settability lives
  // on the binding, held privately, never reified into the program's data.

  function descriptorField(descriptor, name) {
    for (const field of descriptor.fields) {
      if (field.name === name) {
        return field
      }
    }
    return undefined
  }

  // A reversible sub-binding for a RECORD field: read o[name]; write a shallow copy of
  // o with name replaced (other fields preserved).
  function recordFieldBinding(binding, name) {
    return Reactive._objectField(
      binding,
      (o) => (o == null ? undefined : o[name]),
      (o, v) => {
        const base = o == null ? {} : o
        const copy = Object.assign({}, base)
        copy[name] = v
        return copy
      },
      name
    )
  }

  // A reversible sub-binding for a UNION variant: read o[tag]; write a NEW union that
  // holds only this variant (so setting a variant's payload also fixes the tag, and
  // switching variant is just setting a different one).
  function unionVariantBinding(binding, tag) {
    return Reactive._objectField(
      binding,
      (o) => (o == null ? undefined : o[tag]),
      (o, v) => {
        const replaced = {}
        replaced[tag] = v
        return replaced
      },
      tag
    )
  }

  Reactive._typed = function (binding, descriptor) {
    if (binding != null && typeof binding === 'object') {
      binding[TYPE_SYMBOL] = descriptor
    }
    if (descriptor == null || descriptor.kind === 'primitive') {
      return binding
    }

    const handler = {
      get(target, prop, receiver) {
        if (typeof prop !== 'string') {
          return Reflect.get(target, prop, receiver)
        }
        const field = descriptorField(descriptor, prop)
        if (field == null) {
          return Reflect.get(target, prop, receiver)
        }
        const fieldDescriptor = Reactive.DescriptorOf(field.type)
        const sub = descriptor.kind === 'union'
          ? unionVariantBinding(target, prop)
          : recordFieldBinding(target, prop)
        return Reactive._typed(sub, fieldDescriptor)
      },
    }
    return new Proxy(binding, handler)
  }

Section 6 - contexts: Emit / Gather (collect UP)

  // ==========================================================================
  // Section 6 - contexts: Emit / Gather (collect UP)
  // ==========================================================================
  //
  // There is no dynamic provide/use and no closures-broken-free context: a value
  // reaches deeper code only by ordinary closure capture. The ONE cross-scope channel
  // is Emit/Gather, which collects values UP the call stack, scoped by pseudo-modules
  // so it stays local and statically trackable:
  //
  //   Reactive.Emit(channel, value)  - contribute a value to `channel` in the current
  //                                    function's emission frame.
  //   Reactive.Gather(channel)       - drain `channel` from the current frame and
  //                                    return the values (this scope and deeper).
  //
  // Every running Reactive.Function has an emission frame; on return, un-gathered
  // channels bubble into the caller's frame. So a Gather placed after a builder collects
  // everything emitted at that scope-or-deeper, and a value not caught keeps bubbling to
  // the terminal Driver at the root (Section 7). Channel names are short and scoped to
  // the emitting function's MODULE ('content' inside Dom is 'Dom.content').
  //
  // Legality (enforced by the static pass; for now an orphan at the root throws):
  //   - a PLAIN emit is legal only from a pseudo-module function whose defining function
  //     gathers the channel; it goes to that immediate gatherer, never further.
  //   - a RELAY (gather X then emit X in the SAME function) is legal only if a terminal
  //     exists above: a module Driver(X), or an enclosing function that gathers X. This
  //     is the recursion case (Tag gathers its children, then emits itself upward).

  // A channel is TYPED: its type is the type of ONE element (the array a gather returns is a
  // list of that type). Emit(type)('name', value) / Gather(type)('name') / EmitArray(type)
  // ('name', values) are curried like every other generic (State(type)(value)). The type is
  // enforced at BOTH ends: an emit validates its value, a gather validates every element it
  // drains - so a wrongly-typed emission is caught the moment it is gathered, which the static
  // pass (Reactive.Mount) surfaces before the program renders. The bare-string forms
  // Emit('name', value) / Gather('name') remain (untyped = Reactive.Any, no check) so existing
  // modules keep working during migration.
  function currentFrameFor(op, channel) {
    const frame = Reactive._emissionFrameStack[Reactive._emissionFrameStack.length - 1]
    if (frame == null) {
      throw new Error('Reactive.' + op + '("' + channel + '") called outside a run')
    }
    return frame
  }

  function pushEmission(channel, value, optionalType) {
    const frame = currentFrameFor('Emit', channel)
    const qualified = qualifyChannel(channel)
    if (optionalType != null && strictTypes && value != null && value[TYPE_SYMBOL] == null) {
      const descriptor = Reactive.DescriptorOf(optionalType)
      if (descriptor != null) {
        validateAgainstDescriptor(value, descriptor, 'Emit(' + qualified + ')')
      }
    }
    if (frame[qualified] == null) {
      frame[qualified] = []
    }
    frame[qualified].push(value)
  }

  function drainGather(channel, optionalType) {
    const frame = currentFrameFor('Gather', channel)
    const qualified = qualifyChannel(channel)
    const gathered = frame[qualified] == null ? [] : frame[qualified]
    frame[qualified] = []
    if (optionalType != null && strictTypes) {
      const descriptor = Reactive.DescriptorOf(optionalType)
      if (descriptor != null) {
        for (const element of gathered) {
          if (element != null && element[TYPE_SYMBOL] == null) {
            validateAgainstDescriptor(element, descriptor, 'Gather(' + qualified + ')')
          }
        }
      }
    }
    return gathered
  }

  Reactive.Emit = function (typeOrChannel, optionalValue) {
    if (typeof typeOrChannel === 'string') {
      pushEmission(typeOrChannel, optionalValue, null)
      return
    }
    const type = typeOrChannel
    return function (channel, value) {
      pushEmission(channel, value, type)
    }
  }

  Reactive.EmitArray = function (typeOrChannel, optionalArray) {
    if (typeof typeOrChannel === 'string') {
      for (const element of optionalArray) {
        pushEmission(typeOrChannel, element, null)
      }
      return
    }
    const type = typeOrChannel
    return function (channel, values) {
      if (!Array.isArray(values)) {
        throw new Error('Reactive.EmitArray("' + channel + '") expects an array')
      }
      for (const element of values) {
        pushEmission(channel, element, type)
      }
    }
  }

  Reactive.Gather = function (typeOrChannel) {
    if (typeof typeOrChannel === 'string') {
      return drainGather(typeOrChannel, null)
    }
    const type = typeOrChannel
    return function (channel) {
      return drainGather(channel, type)
    }
  }

Section 7 - drivers and the run loop

  // ==========================================================================
  // Section 7 - drivers and the run loop
  // ==========================================================================
  //
  // A driver is a module-scoped TERMINAL gather: whatever a module's functions emit to
  // a channel and no function catches, the driver receives. Reactive.Driver(channel,
  // factory) registers it in the module. `factory(params, addUpdates)` runs ONCE at
  // startup (params come from Mount), keeps its own state in closure vars, and returns
  // the per-run function fed the latest emissions (or an object with a `run` method).
  // Async work (a frame tick, a fetch, a DOM event) calls addUpdates(updates) to queue
  // state updates and schedule the next run; a driver must NOT call addUpdates while
  // processing emissions synchronously. Three shapes fit this one contract:
  //   - shared-resource (Animate): one rAF loop, per-id state, ticks -> addUpdates
  //   - independent    (Network) : one fetch per id, resolve -> addUpdates
  //   - tree/root      (Dom)     : reconcile the emitted node tree into a root element,
  //                                DOM events -> addUpdates; params carry the root
  //                                element and any custom-component handlers.

  // A driver is the ROOT gather for a bare channel name (frame-scoped channels, doc 14 sec 6):
  // register under the bare name, not module-qualified. Driver channel names must be globally
  // unique (content / request / real); a name that reaches the root uncaught goes to its driver.
  Reactive.Driver = function (channel, factory, optionalAlwaysActive) {
    driverRegistry[channel] = factory
    if (optionalAlwaysActive === true) {
      alwaysActiveDrivers[channel] = true
    }
  }

  // The always-active 'calculate' driver. Every Reactive.Calculate emits its callback here; the
  // driver runs each callback (Reactive._runCalculation combines the input locks), and writes the
  // result into its settable - kept locked while its inputs are locked. A lastSignature-per-result
  // guard stops re-runs once a result is stable (no infinite loop). Values that do not serialise
  // fall back to a stable string so they are treated as unchanged rather than looping.
  Reactive.Driver('calculate', (params, addUpdates) => {
    const lastSignature = {}
    return (operations) => {
      const updates = {}
      for (const operation of operations) {
        const computed = Reactive._runCalculation(operation.callback)
        const resultId = Reactive.Reference(operation.result)
        let valueSignature
        try {
          valueSignature = operation.callback == null ? 'n' : JSON.stringify(computed.value)
        } catch (e) {
          valueSignature = String(computed.value)
        }
        const signature = (computed.optionalLock != null ? 'L:' : 'V:') + (valueSignature === undefined ? 'u' : valueSignature)
        if (lastSignature[resultId] === signature) {
          continue
        }
        lastSignature[resultId] = signature
        const gettable = new ReactiveGettable(computed.optionalLock, computed.value)
        const settable = new ReactiveSettable(resultId, undefined)
        updates[resultId] = new ReactiveValue(gettable, settable)
      }
      if (Object.keys(updates).length > 0) {
        addUpdates(updates)
      }
    }
  }, true)

  // Reactive.Mount(contentCallback, driverConfig): driverConfig is a map of qualified
  // channel name -> params for that driver (e.g. { 'Dom.content': { root, components } }).
  // Each named driver is created once; after every Run its channel is drained from the
  // root emissions and handed to its per-run function. Returns { addUpdates }.
  Reactive.Mount = function (contentCallback, driverConfig) {
    const config = driverConfig == null ? {} : driverConfig

    let program = null
    let pendingUpdates = {}
    let scheduled = false

    function addUpdates(updates) {
      Object.assign(pendingUpdates, updates)
      schedule()
    }

    function schedule() {
      if (scheduled) {
        return
      }
      scheduled = true
      Promise.resolve().then(() => {
        scheduled = false
        render()
      })
    }

    const active = []
    const activeChannels = {}
    for (const configKey of Object.keys(config)) {
      // Channels are bare (frame-scoped). A config key is normalised to its bare last segment,
      // so both the new form ('content') and the legacy module-qualified form ('Dom.content')
      // resolve to the same bare driver + root emissions.
      const channel = configKey.indexOf('.') === -1 ? configKey : configKey.slice(configKey.lastIndexOf('.') + 1)
      const entry = config[configKey]
      // A driver is SWAPPABLE: `entry.driver`, if present, is a replacement factory that
      // overrides the module-registered one for this channel. Same contract -
      // factory(params, addUpdates) -> process(emissions) (or { run }). This is how a
      // program's capabilities are controlled, how drivers are simulated, how the UI is
      // captured instead of rendered, and (later) how output is projected to another host.
      const optionalOverride = (entry != null && typeof entry === 'object') ? entry.driver : undefined
      const factory = optionalOverride != null ? optionalOverride : driverRegistry[channel]
      if (factory == null) {
        throw new Error('Reactive.Mount: no driver registered or provided for channel "' + channel + '"')
      }
      const made = factory(entry, addUpdates)
      const process = typeof made === 'function' ? made : made.run
      active.push({ channel: channel, process: process })
      activeChannels[channel] = true
    }

    // Activate the core always-on drivers (e.g. 'calculate') that every program needs and that
    // take no params, unless the config already provided one.
    for (const channel of Object.keys(alwaysActiveDrivers)) {
      if (activeChannels[channel] !== true) {
        const madeAlways = driverRegistry[channel]({}, addUpdates)
        const processAlways = typeof madeAlways === 'function' ? madeAlways : madeAlways.run
        active.push({ channel: channel, process: processAlways })
        activeChannels[channel] = true
      }
    }

    function render() {
      if (program === null) {
        program = Reactive.Program(contentCallback)
      }

      const updates = pendingUpdates
      pendingUpdates = {}
      const hasUpdates = Object.keys(updates).length > 0
      program = Reactive.Run(program, hasUpdates ? updates : null)

      const root = Reactive._rootEmissions
      // ONE delivery path for every driver: it drains its channel from the root frame. A
      // structural region (Dom, and later Scene/Audio) gathers its own subtree and RETURNS it
      // as a { channel: [values] } map - e.g. { content: nodeList }. We fold that map into the
      // root emissions here, so a region's output reaches its driver by exactly the same route
      // as an ambient signal that its producer (Animate 'real', Network 'request') emitted to
      // the root directly. No channel is privileged: 'content' is just a key, and a program may
      // drive several targets at once (return { content: ..., scene: ... }).
      const returned = Reactive._programResult
      if (returned != null && typeof returned === 'object' && !Array.isArray(returned)) {
        for (const channel of Object.keys(returned)) {
          const values = returned[channel]
          if (Array.isArray(values) && values.length > 0) {
            root[channel] = (root[channel] == null ? [] : root[channel]).concat(values)
          }
        }
      }
      for (const driver of active) {
        const emissions = root[driver.channel] == null ? [] : root[driver.channel]
        driver.process(emissions)
      }
      // Orphan guard (until the static pass): an emission that reached the root with no
      // driver to catch it is an unterminated relay - a bug, not a silent drop.
      for (const channel of Object.keys(root)) {
        if (activeChannels[channel] !== true && root[channel].length > 0) {
          throw new Error('Reactive.Mount: orphan emissions on channel "' + channel + '" (no terminal driver)')
        }
      }
    }

    // STATIC TYPE-CHECK PASS. Before any driver sees output, run the whole forward program once
    // in TYPE-CHECK MODE: every branch is followed (If bodies run regardless of condition, Each
    // callbacks run once), reified functions are memoised by identity so recursion terminates,
    // and typed Emit/Gather validate their values - so a channel whose emits do not match its
    // gather's declared element type throws HERE, on ANY path, and the program never starts
    // rendering. The pass's result is discarded (drivers are only invoked by render()). Because
    // component bodies are runtime closures this is a whole-program validation RUN, not a
    // source-level static analysis; a memoised call site is checked once, not per argument shape.
    if (strictTypes) {
      Reactive._typeChecking = true
      Reactive._typeCheckDepth = 0
      try {
        const typeCheckProgram = Reactive.Program(contentCallback)
        Reactive.Run(typeCheckProgram, null)
      } finally {
        Reactive._typeChecking = false
        Reactive._typeCheckDepth = 0
      }
    }

    render()
    return { addUpdates: addUpdates }
  }

Internal handle for the sibling driver modules (Dom.js) and headless checks.

  // ==========================================================================
  // Internal handle for the sibling driver modules (Dom.js) and headless checks.
  // Not for application code.
  // ==========================================================================
  Reactive._internal = {
    ReactiveValue: ReactiveValue,
    ReactiveGettable: ReactiveGettable,
    ReactiveSettable: ReactiveSettable,
    ReactiveLock: ReactiveLock,
    ReactiveError: ReactiveError,
    ReactiveArray: ReactiveArray,
    ReactiveArrayElement: ReactiveArrayElement,
    Security: Security,
    TYPE_SYMBOL: TYPE_SYMBOL,
    validateAgainstDescriptor: validateAgainstDescriptor,
    driverRegistry: driverRegistry,
    invokeCallbackWithArguments: invokeCallbackWithArguments,
  }

TODO (deferred): MUTABLE CONTEXT

  // ==========================================================================
  // TODO (deferred): MUTABLE CONTEXT
  // ==========================================================================
  // Everything above is write-once: Emit/Gather collect, closures read. We have no
  // notion of a context VALUE that can be mutated and re-read (like a context that
  // updates), nor of PEEKING at what has already been emitted into a gather to read its
  // latest value. Nothing in the current surface (Dom tree, Select/Option, the editor
  // panels, the Animate/Network/Dom drivers) needs it, so it is intentionally absent.
  // If a future feature does, the likely shapes are either a mutable-value context or a
  // gather-peek that returns the current accumulation without draining. Add here.
  // ==========================================================================

  globalScope.Reactive = Reactive
})()