#eLucid8 Playground Tutorial
The eLucid8 Playground runs the eLucid8 engine entirely in your browser: you write a program, give it data, ask for one value at one point, and see every demand the engine made to answer it. This tutorial teaches the playground itself. The language is covered in the eLucid8 Tutorial, so read that first if fby, next and @ (t) are new to you.
Each lesson takes 5–10 minutes and starts from one of the built-in examples. Nothing you do is saved: reloading the page or picking another example starts fresh, and other people with the link have their own separate copy.
This file is the source; a shared copy is kept as a Claude Docs page. The numbers and messages it quotes are checked against the engine by packages/evaluator/test/playground-tutorial.test.ts.
#Tour of the screen
The page has a header and three panes: what to compute (Program), what to ask (Query), and why the answer came out as it did (Details). On a narrow screen the panes stack.
| Area | What it holds |
|---|---|
| Header | Examples dropdown (15 ready-made programs) and Reset caches |
| Program pane | The program editor; a status line ("Compiled · 3 values, 1 source…" or a compile error with a clickable line number); one data box per source and per clock; External service latency (ms); the list of external services you can call, with a folded Services reference |
| Query pane | Value dropdown, one coordinate box per dimension, Run query, eight option checkboxes, the result line, and the Demand tree |
| Details pane | The selected tree node explained: how it was produced, its outcome, and any external service call or alignment record; for 2-D values, a grid view |
Edits to the program or data recompile after a short pause, but they do not rerun the query. Press Run query (or Enter in a coordinate box) to run it; changing a checkbox reruns it automatically, except trace, which applies from the next run.
#Lesson 1: Run your first query
You will ask for the running sum of a six-number stream and get 14.
- Choose Streams: first, next, fby from Examples. The program declares
dimension t : int, one sourcex @ (t)and four values:n,running,deltaandsince_start. - Look at source x in the Program pane. It is a table:
t,valuethen the rows 0,3 · 1,1 · 2,4 · 3,1 · 4,5 · 5,9. - In the Query pane, Value is
running @ (t)and t is 4. The example has already run, so the result line reads 14 (3 + 1 + 4 + 1 + 5). - Under the answer, the grey chips count the work: how many values were evaluated, how many source points were read in how many calls, and how many service calls were made.
- Change t to 5 and press Enter: 23. Change it to 6: Undefined, because
next.t xat t = 5 asks for x at t = 6, which the table does not list.
Try the other values at t = 4: delta is 4 (9 − 5), since_start is 2 (5 − 3) and n is 4.
#Lesson 2: Read the demand tree
The demand tree shows every value the engine asked for to answer your query, nested under whatever asked for it. It needs trace on, which is off by default to keep big queries fast.
- Stay on Streams with
runningat t = 4. Under Demand tree you will see "Trace is off". Press Turn on the trace and re-run. - Then press Reset caches: the first run already stored the answer, and without the reset the tree would show only that stored copy.
- The root row is
runningat t = 4. Each row shows the point asked for, a coloured label saying how it was produced, and→its outcome. The first three levels open automatically; click ▸ to open deeper ones. - Under the root you will find
runningat t = 3 (thefbystep back) andxat t = 4 labelled source-read. Followrunningdown to t = 0 to see the whole recursion. - Click any row. The Details pane names it, explains its label in one sentence and shows its outcome.
- Press Run query again without changing anything. The root is now labelled cache-hit with no children: the answer came from the point cache (Lesson 4).
For a value with two or more integer dimensions, Details also offers Show value over x × y: a coloured grid around your point. If the value has a third dimension, ◀ ▶ and Play step through it. Try it on Heat diffusion over time and space to watch the heat spread tick by tick.
#Lesson 3: Write your own program and give it data
Every source you declare gets a data box, and you choose how it supplies values. Start from any example and replace the program with this:
dimension t : int
source temp @ (t)
change @ (t) = next.t temp - temp
warming @ (t) = change > 0
- The status line should read "Compiled · 2 values, 1 source". If it shows a compile error instead, click the line number in the message to jump to the problem.
- A source temp box appears with the header
t,value. Add the rows 0,10 · 1,12 · 2,11 · 3,15, one per line. - Pick
warming @ (t), set t to 2, press Run query: true (15 − 11 = 4). At t = 3 it is Undefined, because there is no t = 4 row. - Switch the box's mode from table (CSV) to formula and enter
10 + #t * 0.5. Now every t has a value, sochangeis 0.5 everywhere.
The three data modes:
| Mode | What you type | Points with no data |
|---|---|---|
| table (CSV) | A header of the source's dimensions plus value, then one row per point. Values can be numbers, true/false, text, undefined or error: message. A quoted field ("a, b", with "" for a quote) is always text; read exactly as the elucid8 command reads a CSV file | Undefined |
| formula | One eLucid8 expression using #t, #x and so on | None: every point has a value |
| built-in example | Nothing; offered only where an example supplies its own function (the Alps data, the Game of Life seed) | Decided by that function |
A program that declares a clock gets a clock box too, starting at 0, 100, 200; Lesson 5 covers it. Editing a program or its data never clears the caches. Each edit creates a new version, and stored values from the old version simply stop matching.
#Lesson 4: Caches and Reset caches
The playground keeps computed values in two places. The memo table lives for one query; the point cache lives in the page and lets later queries reuse earlier work.
| Memo table | Point cache | |
|---|---|---|
| Lasts | One query | Until Reset caches, a new example, or a page reload |
| Key | name + context | name + definition version + context |
| Checked on a hit | No: one query sees one snapshot of the data | Yes: the versions of the sources and clocks it was built from must still match |
| Tree label | memoized | cache-hit |
| Switched by | Always on | the point cache checkbox |
- On Streams, with trace on, press Reset caches. Query
runningat t = 4: every row is computed or read. - Query t = 5. The root is computed, but its child
runningat t = 4 is a cache-hit: the earlier query's work was reused. - Edit source x, changing the first row to
0,4, then run t = 5 again. The answer becomes 24 and everything is recomputed: the old entries were built from a different version of x, so they no longer count. - Untick point cache and run again. With no page cache, the whole recursion is computed every time.
Reset caches clears only this page in your browser: the point cache, the cluster store used by tiling, the source cache and the counts that decide when tiling starts. Then it reruns the current query. Nothing is saved anywhere else.
#Lesson 5: Clocks, service calls and late
A clock gives each tick of a dimension a timestamp in milliseconds. An effect calls an external service: a program outside eLucid8 that answers on request, such as an AI model. A source is external data you read; an effect calls an external service that computes an answer. Its deadline counts from the tick's timestamp, and a call that finishes after the deadline is late, which is an error.
- Choose Spatial anomaly (README workload). Its program declares
dimension time : int clock sensor, and the clock sensor box holds0, 100, 200, 300: tick 0 is at 0 ms, tick 1 at 100 ms, and so on. - The effect
scorecalls the serviceanomaly_detectorwithdeadline 20msandretry overloaded up to 2(at most 2 attempts in total). At tick 1 its deadline is 100 + 20 = 120 ms. - The default query,
alarmat time 1, x 7, y 6, comes back late: the detector finished at 121 ms. Turn on trace, open thescorerow and read the External service call table in Details: the deadline, each attempt's start and finish, and the result. - Change y to 7 and run: the call finishes in time, and
alarmgets an ordinary answer (false). - Press Show alarm over x × y, then Play. Late cells are purple; the legend names every colour.
Two things a clock does not do. It does not require evenly spaced ticks: any strictly increasing list of whole milliseconds is accepted. And a source with no value at a tick gives Undefined, not late, because sources have no deadline.
The spatial example's detector is built in, with its own timings, so the latency box has no effect on it. The Program pane lists the services you can call, and the Services reference describes each one. For programs you write, the playground supplies a stand-in service, sum_service, which adds up its numeric inputs and takes External service latency (ms) to answer. To see late appear and disappear on demand:
dimension time : int clock sensor
source x @ (time)
effect total @ (time) =
call sum_service(x)
deadline 20ms
Switch source x to formula and enter #time. The clock box starts at 0, 100, 200. Query total at time 2: the answer is 2, delivered 5 ms after the 200 ms tick. Set the latency to 30 and run again: late (230 ms > deadline 220 ms).
Only services the playground supplies can be called. Misspell the name, call sum_servce(x), and the status line shows a compile error, "unknown service: sum_servce; available: sum_service", with the line to fix. (In the Spatial example the list also includes anomaly_detector.)
#Lesson 6: Align two clocks
Values on different clocks can only be combined through align, which picks ticks on the other clock by timestamp. The policy decides which ticks count.
Choose Two clocks and alignment. A reading arrives on clock sensor (0, 98, 203, 300, 390, 401, 499 ms) and is multiplied by a calibration on clock calib (0, 230, 380, 401 ms). The calibration table is 1.0, 1.05, undefined, 1.1, so the tick at 380 ms is missing.
| time_s (sensor ms) | asof | hold max 100ms | window 200ms (mean) |
|---|---|---|---|
| 1 (98) | 11 | 11 | 11 |
| 2 (203) | 12 | Undefined: last value is 203 ms old | Undefined: no calib tick in the window |
| 3 (300) | 13.65 | 13.65 | 13.65 |
| 4 (390) | Undefined: tick at 380 has no value | Undefined: 1.05 is 160 ms old | 14.7: the missing value is skipped |
| 5 (401) | 16.5 | 16.5 | 16.125: mean of 1.05 and 1.1 |
- Query each of the three values at time_s 1 to 5 and compare with the table.
- With trace on, open the aligned row under any answer. Details lists the calib ticks examined, in order, with their timestamps, or "no tick qualified".
- Edit the calib clock or the calibration table and watch the answers change. Lesson 4 applies: every stored value built on the old clock version is recomputed.
The fourth policy, exact, matches only a calib tick with exactly the same timestamp: here only time_s 0 and 5.
#Lesson 7: Speed options never change the answer
Five checkboxes change how the engine works, never what it computes. Choose Image pipeline: blur and edges: the query edge at x 16, y 7 is 0.444 under every combination. Watch the chips under the answer instead.
Turn the options on one at a time, pressing Reset caches after each. These are the counts for the first run, with check mode on (the default):
| Options on | Evaluated | Source points / calls | Tree label | What changed |
|---|---|---|---|---|
| None extra (point cache, check) | 7 | 21 / 21 | computed | Only what the query needs |
| + tiling (size 4) | 128 | 126 / 126 | tile-fill | The whole 4×4 tile around the point is computed and stored |
| + batched reads | 128 | 126 / 1 | tile-fill | The same reads, gathered into one call |
| + tile kernels (with tiling) | 512 | 190 / 65 | vectorised | Whole tiles run as one kernel instead of point by point |
Check mode verifies each kernel by evaluating every element again point by point, and those extra evaluations and reads are counted too. Untick check mode on the last row and the counts show what the kernel itself did: 96 evaluated, 64 source points in 1 call.
- With tiling on, query x 17, y 6: cluster-hit, 0 evaluated, because it sat in the tile just filled. Query x 20, y 7: a new tile is filled.
- Untick tiling and point cache, then turn on source cache. Query x 17, then x 16: the second query reads only 5 source points instead of 21, because neighbouring reads are kept between queries. Query x 17 again: 0.
- On a vectorised row, Details offers Re-run this element per element, without kernels, which shows the full per-point tree for the same answer.
Tiling pays off when you will ask for neighbouring points; for one point it does extra work. Tile size (2, 4 or 8) sets the tile's width in each dimension.
The fifth option, group levels (on by default), speeds up values that recur together over a dimension, such as a network's weights and gradients over training steps. Each step is computed as a few whole-array kernels instead of point by point. Try it on Training a tiny neural network (XOR): untick check mode, then run predict with group levels on and off. The answer is the same; with it on, the query runs about 3× faster (about 7 ms against 20 ms on a laptop).
#Lesson 8: Undefined, errors and check mode
An answer is a value, Undefined or an Error, and every Error names the point where it started. Choose Missing and failed readings (Undefined and Error): reading has no value at hour 5 and divides by zero at hour 9.
| Query | Answer | Why |
|---|---|---|
doubled at t = 5 | Undefined | Strict: a missing input gives a missing result |
doubled at t = 9 | Error via reading (t = 9): division by zero | The Error names its source point |
safe at t = 9 | 0 | The guard is false, so the bad reading is never asked for |
window3 at t = 6 | Undefined | The window t 5–7 includes the missing hour |
window3 at t = 10 | Error via reading (t = 9): division by zero | The window t 9–11 includes the failed hour |
The playground catches three kinds of program mistake:
- Compile errors appear in the status line with a clickable line number, and the result reads "Fix the compile error first." Try deleting the end of a line.
- Unguarded cycles are caught at compile time.
a @ (t) = b + 1withb @ (t) = agives "unguarded cycle: a -> b -> a". - Runaway recursion stops at 100,000 nested demands.
f @ (t) = f @ {t = #t + 1}ends with "demand depth limit (100000) exceeded … (recursion that is not well founded?)".
Check mode (on by default) makes the engine audit itself while it runs. It confirms that cache and cluster entries for the same point agree, that kernels give exactly what point-by-point evaluation gives, that a value freed from memory and computed again comes out the same, and that remembered validity matches a fresh check. A check failure is not a mistake in your program; it means the engine broke one of its own rules, and the message names the rule (R1, R3, M2r and so on). Report it with the program and query that caused it. Check mode makes queries slower, because it repeats work to compare; untick it to see the engine's real speed.
#Quick reference
Options
| Checkbox | Default | Effect |
|---|---|---|
| point cache | on | Keeps computed values between queries; reused while their versions match |
| tiling + tile size | off, 4 | On a miss, computes the whole tile around the point |
| batched reads | off | Gathers source reads into as few calls as possible |
| source cache | off | Keeps source values between queries |
| tile kernels (with tiling) | off | Computes a whole tile in one kernel run |
| group levels | on | Computes values that recur together one whole level at a time |
| check mode | on | The engine audits its own rules while it runs (slower) |
| trace | off | Records the demand tree; takes effect on the next run |
Demand tree labels
| Label | Meaning |
|---|---|
| computed | Its equation was evaluated |
| source-read | Read from a source at the query's pinned version |
| source-memo | A source point already read in this query |
| source-cache | A source point read by an earlier query (source cache) |
| cache-hit | Taken from the point cache, still valid |
| cluster-hit | Taken from a tile stored by tiling |
| tile-fill | A miss under tiling: the whole tile was computed |
| vectorised | Computed with its tile or level by one kernel run |
| called | An external service was called |
| memoized | The same point or service call earlier in this query |
| replayed | A successful service result from an earlier query, reused by policy |
| aligned | A value on another clock, reached through align |
Answer colours in the grid view: true, false, late (purple), Undefined (grey), Error (red), and a shaded scale for numbers.