scrml.dev v0.7.1
Reference › Keywords

derived

Read-only reactive binding. The RHS is an expression that recomputes whenever any dependency changes; the bound name is never directly written.

What it means

A derived cell is the third state shape (per SPEC §6.6): its value is a pure function of other reactive bindings. The compiler tracks the dependencies automatically; on any dep change, the cell re-evaluates and downstream consumers re-render.

Two non-negotiable rules:

  • Derived cells are write-forbidden. Assigning into a derived cell fires E-DERIVED-WRITE (§6.6.8) at compile time. In-place mutation (@derivedArr.push(x)) fires the sibling error E-DERIVED-VALUE-MUTATE (§6.6.18). Mutate the upstream cell instead.
  • Validators are forbidden on derived cells. Predicates apply to inputs; derived outputs don't have an independent validity surface. Adding req to a derived cell fires E-DERIVED-WITH-VALIDATORS .

Syntax

The const <name> = expr form is the SOLE declaration syntax for derived reactive values (§6.6.1). The @derived keyword form is explicitly NOT recognised — attempting it fires E-REACTIVE-001 with a hint suggesting const <name> = expr. (The derived=expr attribute on <engine> is a separate construct — see the engine form below.)

// Numeric derived
const <doubled> = @count * 2

// Markup-typed derived (§6.6.17)
const <badge>   = <span class="badge">${@user.name}</span>

// String derived
const <greeting> = "Hello, " + @user.name

Derived engines (Tier 2 form):

<engine for=Phase derived=computePhase(@a, @b)>
  <Idle/>
  <Loading/>
  <Ready/>
</engine>

Derived engines never carry initial= (fires E-DERIVED-ENGINE-NO-INITIAL ) and never accept writes — the variant is the output of the derived expression.

Worked example

Live search. A search input drives a derived @filtered cell; the ${@filtered} list re-renders automatically on every keystroke. No manual subscribe, no useEffect, no computed() wrapper.

<program>

  <contacts> = [
    { name: "Alice",   email: "alice@example.com"   },
    { name: "Bob",     email: "bob@example.com"     },
    { name: "Charlie", email: "charlie@example.com" },
  ]
  <query> = ""

  const <filtered> = @contacts.filter(c => c.name.toLowerCase().includes(@query.toLowerCase()))

  <input bind:value=@query placeholder="Search by name">

  <ul>
    ${ for (let c of @filtered) { lift <li>${c.name} — ${c.email}</li> } }
  </ul>

</program>

Notice: the @filtered cell never gets a write site. It's computed from @contacts and @query; both are tracked automatically. If you tried @filtered = [...] anywhere, the compiler would reject it at compile time (E-DERIVED-WRITE).

Semantics

  • Lazy evaluation by default. The derived expression evaluates on first read. If nothing reads it, it doesn't run.
  • Dep tracking is static, not runtime. Every @x read appearing inside the derived RHS becomes a compile-time dependency — regardless of conditional reachability. Both branches of an if contribute their reads to the dep set. Runtime auto-tracking (SolidJS-style branch-conditional subscription) is explicitly rejected by §6.6.3 for const <name> = expr cells outside ^{} meta blocks.
  • Result caching. The computed value is cached. Consumers re-read the cached value without re-running the expression until a dep changes.
  • Markup-typed derived cells are first-class. A derived expression whose RHS produces markup (per §6.6.17) is a markup-typed value — interpolatable, slot-fillable, return-valuable. The same way a string-typed derived cell is a string.
  • Reactivity attributes are forbidden. debounced= / throttled= on a derived cell is E-DEBOUNCED-WITH-DERIVED. Wrap the upstream input instead.

Edge cases

Don't mutate the value, even if the type is mutable

@derivedArr.push(x) on a const-derived array is E-DERIVED-VALUE-MUTATE. Even if the mutation would technically work (JS arrays are mutable), the next dep-fire will recompute the value and clobber the push. Mutate the upstream cell: @items = [...@items, x].

Self-reference is a cycle

A derived cell that reads itself in its own RHS is a structural cycle. The compiler detects this at the dep-graph pass and rejects.

Don't put side effects in the RHS

Derived RHS expressions should be pure functions of their deps. Writing to other cells inside the RHS, doing IO, calling fetch() — all of these break the "this value IS this function of these inputs" contract. Side effects belong in event handlers or <onTransition> elements, not derived RHS.

Related

Specification

Normative text: SPEC §6.6 (Shape 3 — derived) + §6.6.17 (markup-typed derived) + §6.6.18 (derived value-mutate forbidden) + §51 (derived engines). Spec lives at compiler/SPEC.md .