Documentation / Reactivity

Reactivity

Reactivity lets a template observe a stable source without turning Moth into a callback language.

Sources and subscriptions stay explicit. Mutation follows the ordinary type and access rules.

Choose the explanation level: Reactive sources

Reactive sources

A reactive source is a value that templates can watch for changes.

A counter source


    count $Int = 0
    

$Int says count is reactive storage for an Int.

Inferred type


    enabled $= true
    

$= lets the compiler infer the underlying type from the initializer.

Snapshot read


    count $Int = 0
    current = count
    

Reading count normally gives the current value. It is a snapshot, not a live view.

A reactive source is stable mutation-capable runtime storage declared with $Type or $=.

Syntax


    count $Int = 0
    enabled $= true
    names ${String} = {"Priya"}
    scores ${String = Int} = {}
    

Exact contract

  • $Type declares a reactive source with an explicit underlying type.
  • $= infers the underlying type from the initializer.
  • Reactive syntax is access and storage metadata, not a first-class wrapper type.
  • Semantic type identity remains the underlying ordinary type.
  • The declaration owns stable mutation-capable storage in its runtime scope.
  • Source identity belongs to the binding.
  • Ordinary reads snapshot the current value.
  • A reactive declaration is runtime storage, not a compile-time constant.
  • Normal no-shadowing and type compatibility rules apply.
  • Collection and map source values retain their ordinary collection and map semantics.

Explicit and inferred types


    count $Int = 0
    enabled $= true
    

Both forms create the same kind of source. $= suits cases where the initialiser makes the type obvious.

Snapshot reads


    count $Int = 0
    snapshot = count
    

A plain read of a reactive source evaluates to its current value. It is a snapshot, not a live dependency.

Reactive values are not a second type system

A $Int source stores an Int. You can pass its current value anywhere an Int is expected. The $ only marks the binding as reactive storage.

Invalid positions

Reactive syntax is not valid in struct fields, choice payloads, return slots or type aliases unless another accepted surface explicitly defines it. Function parameters are the only other accepted position. See Reactive parameters.

Related concepts

Choose the explanation level: Subscriptions

Subscriptions

A subscription tells a template to watch a reactive source and update when it changes.

Snapshot versus subscription


    count $Int = 0

    once = [: [count]]
    live = [: [$(count)]]
    

A plain capture reads the value once. A dollar-paren capture creates a live subscription.

One bare source

A subscription must name exactly one reactive source identifier.


    count $Int = 0

    view = [: Count: [$(count)]]
    

Not an expression

You cannot subscribe to a field path, a function call, an arithmetic expression or mutable access. Those forms are deferred.

A template subscription creates a read-only live dependency on a reactive source.

Syntax


    count $Int = 0

    view = [: Count: [$(count)]]
    

Exact contract

  • A subscription is valid only in template head or capture positions accepted by V1.
  • It accepts exactly one bare reactive source identifier.
  • It captures stable source identity and read-only dependency metadata.
  • It does not capture a mutable borrow.
  • It does not copy the value.
  • It does not evaluate a computed expression.
  • An ordinary template capture is a snapshot.
  • The dollar sign must be immediately followed by an opening parenthesis.
  • Whitespace-separated dollar-paren is not subscription syntax.

Invalid operands

The following are deferred or invalid:

  • field paths such as a source field access
  • collection or map operations such as an indexed get call
  • function calls
  • arithmetic or other expressions
  • mutable access
  • empty argument
  • multiple arguments

Invalid contexts

Subscriptions are invalid in:

  • compile-time templates
  • constants
  • ordinary expression positions
  • non-template syntax positions

Snapshot versus subscription


    count $Int = 0

    snapshot = [: [count]]
    live = [: [$(count)]]
    

The first reads the value once. The second records a dependency that may rerender when the source changes.

Related concepts

Choose the explanation level: Reactive parameters

Reactive parameters

A reactive parameter lets a function receive a live source instead of a snapshot.

Reactive parameter


    counter_view |count $Int| -> String:
        return [: Count: [$(count)]]
    ;

    count $Int = 0
    view = counter_view(count)
    

$Int means the function receives a reactive source of Int. Inside the function, a subscription to the parameter can stay live.

Ordinary parameter


    show |count Int| -> String:
        return [: Count: [count]]
    ;

    count $Int = 0
    view = show(count)
    

An ordinary Int parameter reads the current value once.

A reactive parameter passes stable source identity across a function boundary without granting mutation permission.

Syntax


    counter_view |count $Int| -> String:
        return [: Count: [$(count)]]
    ;
    

Exact contract

  • A reactive parameter preserves stable source identity across the function boundary.
  • It is a read and subscription handle.
  • It does not grant mutation permission.
  • A non-reactive value cannot satisfy a reactive parameter.
  • Reactive parameters do not support defaults.
  • Passing a source to an ordinary T parameter performs an ordinary snapshot read.
  • A reactive parameter is not a first-class $T value type.
  • Reactive syntax is not valid in fields, return slots or aliases unless another accepted surface explicitly defines it.

Passing a source to a reactive parameter


    counter_view |count $Int| -> String:
        return [: Count: [$(count)]]
    ;

    count $Int = 0
    view = counter_view(count)
    

Passing a source to an ordinary parameter


    show |count Int| -> String:
        return [: Count: [count]]
    ;

    count $Int = 0
    view = show(count)
    

The source is read once and the snapshot value is passed.

Plain values cannot satisfy reactive parameters


    counter_view |count $Int| -> String:
        return [: Count: [$(count)]]
    ;

    plain Int = 0
    -- Error: plain value cannot satisfy reactive parameter
    -- view = counter_view(plain)
    

Related concepts

Choose the explanation level: Mutation and invalidation

Mutation and invalidation

Changing a reactive source notifies its subscribers.

Assignment


    count $Int = 0
    count = 1
    

Assigning to count updates the source. Anything subscribed to count will know it may need to update.

Mutable collection source


    names ${String} = {"Priya"}

    ~names.push("Grace") catch:
        io.line("push failed")
    ;
    

You need ~ to mutate the collection. Reactivity does not relax the access rules. Fallible operations like push must be handled with catch or propagated inside a compatible Error! function.

Mutating a reactive source updates the same stable storage and invalidates its subscribers.

Assignment


    count $Int = 0
    count = 1
    

Assignment updates the same stable source. Subscribers to that source are marked invalid.

Invalidation in V1

Mutation through a source invalidates the whole source in V1. Invalidating operations include:

  • assignment
  • field writes
  • collection mutation
  • map mutation
  • mutable write-through calls

Field, item and path-level invalidation are deferred.

Mutation still follows access rules


    names ${String} = {"Priya"}

    ~names.push("Grace") catch:
        io.line("push failed")
    ;
    

Mutation still requires explicit ~ for mutable or exclusive receiver access. Reactive syntax does not bypass ordinary borrowing. Fallible mutations must be handled with catch or propagated inside a compatible Error! function. Top-level ! propagation is rejected when no error channel exists.

Subscriptions are not borrows

A subscription is a read-only dependency record. It does not capture a mutable borrow, copy the value or extend a source-language lifetime. Subscriptions do not block later valid mutable access by themselves.

Borrow validation

Borrow validation records source dependencies and conservative invalidation facts. Reactive metadata is separate from active borrow lifetimes.

Related concepts

Choose the explanation level: Runtime sinks

Runtime sinks

A template with a subscription stays a String, but a supported backend can keep it live.

Still a String


    count $Int = 0
    view = [: Count: [$(count)]]
    

view has type String. The compiler keeps extra metadata about the subscription.

HTML-JS live fragment

In an HTML-JS project, a top-level runtime template with a subscription can rerender when the source changes.


    count $Int = 0

    [: [$(count)]]
    

Not every sink is live

io.line and assert do not accept reactive templates. HTML-Wasm rejects reactive runtime features for now.

A runtime template containing a subscription keeps language type String. The compiler preserves dependency metadata so a supported backend can keep the output live.

Metadata propagation

The compiler preserves direct template dependency metadata. Metadata may flow through:

  • template composition
  • assignment
  • return
  • direct argument passing
  • ordinary String parameters inserted into templates

Ordinary string operations do not automatically preserve reactive metadata.

Supported live sink

The first live V1 sink is a top-level runtime HTML fragment in the HTML-JS builder. HTML-JS mounts the reactive template and rerenders the whole mount slot when a dependency invalidates.

Plain string fragments keep ordinary insertion behaviour.

Target gates

  • HTML-JS supports the accepted V1 reactive source and template feature set when it reaches the supported top-level runtime HTML-fragment sink.
  • HTML-Wasm rejects reachable reactive runtime features until it has equivalent support.
  • Unsupported sinks must not silently become live.

Rejected sinks

  • io.line(...) with direct subscription-bearing reactive templates is rejected until the IO and side-effect model is designed.
  • assert(...) messages are string literals only, so reactive or runtime template messages are rejected.

Keep backend helpers out of the language contract

The language contract is the source syntax, the metadata and the supported sink gating. Specific backend helper names and internal render plans are backend artifacts, not user-visible language semantics.

Related concepts

Choose the explanation level: Reactivity scope

Reactivity scope

Reactivity in Moth stays small. It covers UI state, not general callbacks.

Constrained purpose


    count $Int = 0

    [: [$(count)]]
    

You can declare a source, subscribe to it in a template and let the backend rerender when it changes.

Not closures

Moth does not have anonymous function values, generic function values or general closures. Reactivity is the UI-oriented alternative.

Reactivity V1 is a constrained template and UI mechanism. It is not general closure or function-value support.

What V1 covers

  • explicit reactive declarations with $Type and $=
  • reactive parameters with $Type
  • template subscriptions with $(source)
  • whole-source invalidation
  • top-level HTML-JS runtime fragments as live sinks

Deferred

  • reactive template control flow beyond the accepted source and sink model
  • field and path subscriptions
  • collection item subscriptions
  • expression dependency tracking
  • derived reactive values
  • reactive IO sinks
  • fine-grained DOM text, attribute and style updates
  • nested reactive regions
  • keyed loop diffing
  • template-owned event, action or effect syntax
  • $bind(...)
  • typed component events or messages
  • HTML-Wasm runtime support

Outside current design scope

  • general closures
  • anonymous function values
  • generic function values
  • higher-order polymorphism

Backend optimizations may become finer without changing source semantics.

Related concepts