Status: Normative draft (Phase 9–10 — correctness + Scheduler 2.0)
Implementation:vm/runtime/luke_reactive.h
Architecture overview:REACTIVE.md
Roadmap:REACTIVE_ROADMAP.md
This document defines scheduler guarantees for the Luke Reactive Runtime.
Surface syntax may evolve; these semantics are the contract for native and WASM.
1. Graph model
1.1 Node kinds
| Kind | Role | Writes | Reads trigger |
|---|---|---|---|
CELL |
Source value (number/text) | CHANGE, INCREASE, writers |
luke_rx_read_* |
DERIVED |
Pure compute | Never directly | compute fn reads |
EFFECT |
Side effect | May touch UI/I/O | effect fn reads |
LIST / MAP |
Collection sources | ADD, SET ITEM, PUT |
collection accessors |
1.2 Edges
- dep:
from → tomeans from read to during last compute/effect run. - sub: inverse of dep (who to notify when to changes).
- Dynamic deps: recorded only while
g->computing != 0.
1.3 Stale edge cleanup (v0.1)
Before each derived/effect (re)run, the runtime must:
- For each prior dep
dof noden, removenfromd.subs. - Reset
n.dep_lento 0. - Re-register deps during the new run via reads.
Guarantee: No subscriber lists retain edges from superseded dynamic branches.
2. Flush protocol
A flush runs when dirty_len > 0 and batching == 0.
Wave order (fixed)
W1 Propagate dirty → transitive subs marked dirtyW2 Recompute DERIVED (pure), ascending node idW3 Run EFFECT, ascending node idW4 Clear CELL/LIST/MAP dirty flags epoch++, flush_count++W5 after_flush (layout/paint consumers)
2.1 Deterministic derived order
Within wave 2, derived nodes are considered in strict ascending LukeRxId order each iteration.
Among nodes whose derived dependencies are clean, lower id runs first.
Guarantee: Same graph + same writes ⇒ same derived run order (single-threaded).
2.2 Batch coalescing
While batching > 0, writes mark dirty but do not flush.
One batch_end ⇒ at most one flush for all batched writes.
Guarantee: flush_count increments by 1 per completed batch (not per write).
2.3 Cycle detection
If wave 2 makes no progress with pending derived nodes, the runtime:
- Sets
cycle_tripped = 1 - Logs to stderr
- Force-clears derived dirty flags
- Returns
-1fromluke_rx_flush
3. Invalidation
A write to node n:
- Updates value +
version++ - Marks
ndirty (if applicable) - Recursively marks subscribers dirty (transitive)
Derived/effect nodes never write their dependents without going through a cell/collection write.
4. Scopes & disposal
scope_begintracks new nodes inowned[].scope_end(name)disposes owned nodes: unlinks all edges, marksdead = 1.- Dead nodes are skipped in flush and reads return defaults.
5. Instrumentation (v0.1)
| Counter | Meaning |
|---|---|
epoch |
Flush generations |
flush_count |
Completed flushes |
last_flush_derived |
Derived runs in last flush |
last_flush_effects |
Effect runs in last flush |
last_flush_deps_cleared |
Stale dep edges removed in last flush |
total_deps_cleared |
Cumulative stale-edge removals |
granular_paints |
UI row paints (Phase 5 tests) |
Build surface (introspection):
raw "THE EPOCH"raw "THE FLUSH COUNT"raw "THE DERIVED RUN COUNT"raw "THE EFFECT RUN COUNT"raw "THE STALE EDGE COUNT"raw "THE GRANULAR PAINT COUNT"
6. Conformance
Programs under examples/build/reactive_conformance_*.luke assert v0.1+ guarantees on native.
WASM parity: same programs compiled with -target browser must match native outputs (roadmap).
7. Scheduler 2.0 (v0.2)
7.1 Priority lanes
Effects run in ascending priority (lower number = sooner):
| Priority | Kind | Surface |
|---|---|---|
UI (0) |
User-visible / interactive | BIND, BIND LIST, BIND OPACITY, WHEN REACTIVE |
NORMAL (1) |
Derived compute | THE x IS … |
BACKGROUND (2) |
Deferred work | BIND BACKGROUND, WHEN BACKGROUND REACTIVE |
Within the same priority, ascending node id breaks ties (deterministic).
7.2 Nested flush coalescing
While flushing == 1, cell/collection writes must not re-enter luke_rx_flush synchronously.
They set pending_flush = 1 and increment deferred_flush_count.
One outer flush turn:
- Runs one or more internal passes until
pending_flush == 0ordirty_len == 0 - Increments
flush_countonce - Increments
epochonce
Guarantee: last_flush_passes >= 1; nested writes during effects coalesce into the same turn.
7.3 Dirty dedup
mark_dirty on an already-dirty node is a no-op and increments last_flush_dedup_hits during flush.
7.4 Instrumentation (v0.2)
| Counter | Meaning |
|---|---|
last_flush_passes |
Internal passes in last turn |
deferred_flush_count |
Cumulative nested deferrals |
last_flush_dedup_hits |
Dedup hits last pass |
last_dirty_q_size |
dirty_q length at wave 1 |
last_flush_steps |
Timeline entries last turn |
ui_before_bg |
UI effect ran before any BACKGROUND effect |
Build surface:
THE FLUSH PASS COUNTTHE DEFERRED FLUSH COUNTTHE DIRTY DEDUP COUNTTHE SCHEDULER STEP COUNTTHE SCHEDULER UI BEFORE BACKGROUND
8. Granularity (v0.3)
8.1 Region paint
luke_rx_ui_set_text_granular and list row paints call argus_paint_one — only the touched Argus node is presented.
Full argus_paint runs when need_paint is set (e.g. generic BIND).
8.2 Region layout
When need_layout is set (e.g. BIND OPACITY), hanka_mark_region marks the root box containing the leaf.
hanka_layout_dirty relayouts only dirty roots when keep_roots is enabled (reactive UI).
8.3 Component subtree invalidation
On scope_end, external subscribers of owned nodes are marked dirty before disposal (THE SUBTREE INVALID COUNT).
9. Memory management (v0.4)
9.1 Disposal
DESTROY COMPONENT disposes owned nodes (dead = 1), unlinks all edges, clears compute/effect fns.
Dead nodes are skipped in flush/reads (default values returned).
9.2 Weak reads
THE WEAK VALUE OF cell reads without registering a dependency edge during derived/effect compute.
9.3 Leak audit
luke_rx_audit_graph scans alive nodes for deps pointing at dead nodes, repairs them, and updates counters.
AUDIT REACTIVETHE ALIVE NODE COUNTTHE DEAD NODE COUNTTHE DISPOSED COUNTTHE LEAK EDGE COUNTTHE WEAK READ COUNT
9.4 Weak effects
WHEN REACTIVE WEAK cell CHANGES DO registers an effect whose reads do not create dependency edges (same as weak reads for the whole effect body).
9.5 Scope GC
On scope_end / UNMOUNT COMPONENT, closed scope frames with no owned nodes are compacted from the scope table (THE SCOPE GC COUNT, THE SCOPE FRAME COUNT).
10. DevTools (v0.5)
10.1 Why-changed
THE WHY ROOT OF derived walks deps to the nearest source cell (BFS, min id tie-break).
THE WHY DEPTH OF derived is hop count to that root.
TRACE WHY cell logs the chain to stderr and increments THE WHY TRACE COUNT.
THE LAST WRITE ID records the most recent cell/collection write.
10.2 Live graph
DUMP REACTIVE GRAPH prints alive nodes to stderr (THE GRAPH DUMP COUNT).
Counters: THE GRAPH CELL COUNT, THE GRAPH DERIVED COUNT, THE GRAPH EFFECT COUNT, THE GRAPH EDGE COUNT.
10.3 Timeline export
After flush, scheduler steps are readable via:
THE TIMELINE STEP ID AT 0THE TIMELINE STEP WAVE AT 0
Wave 2 = derived, 3 = effect.
12. Error system (v0.6)
12.1 Isolation
When a derived/effect run sets a problem (GIVE UP inside ATTEMPT), the runtime:
- Marks the node
errored = 1 - Clears its deps and removes it from the dirty queue
- Continues flush — sibling effects still run
12.2 Async failure
REPORT REACTIVE FAILURE FOR cell WITH message isolates the cell and increments THE ASYNC FAILURE COUNT.
12.3 Retry
RETRY REACTIVE ERROR clears errored on last_error_node, marks dirty, and flushes.
CLEAR REACTIVE ERROR clears the flag without re-scheduling.
12.4 Error boundaries
BEGIN ERROR BOUNDARY Panel tags nodes/effects created inside with boundary_scope_id.
When a node inside trips, the boundary is marked tripped and remaining nodes in that boundary are skipped for the flush turn.
Nodes outside the boundary continue normally.
RESET ERROR BOUNDARY Panel clears the tripped flag and re-schedules errored nodes in that boundary.
13. Non-normative (future)
Not yet specified: macrotask queues, parallel reactions, time-travel, field-level object tracking, Hanka region invalidation spec.
See REACTIVE_ROADMAP.md.