Learn · from docs/how-it-fits-together.md in the eLucid8 repository

Contents

How eLucid8 fits together

Status: 2026-10-04. A guide to the parts of eLucid8, how they relate, and how to use each one today, with a short hands-on tutorial. For the language itself, see the tutorial; for the browser playground, the Playground Tutorial.

The big picture

You write one program. Everything else is a way of running it, and every way must give exactly the same results.

flowchart LR
  P["program (.el8)"] --> C["compiler (TypeScript)"]
  C --> R["reference evaluator (TypeScript)<br/>the definition; traces; check mode"]
  C --> PL["plan (JSON)<br/>the contract between engines"]
  PL --> JS["fast paths in TypeScript<br/>group levels, fused kernels"]
  PL --> RS["Rust runtime<br/>kernels + eduction + clocks + services"]
  R -. "checks" .-> JS
  R -. "checks" .-> RS
partwhat it is forwhere it lives
Languagevalues over named dimensions; demand-driven; exact outcomes (Value, Undefined, Error)semantics, tutorial
Compilerparses, checks and expands a program; works out what can be computed fast; writes the planpackages/evaluator/src/syntax/
Reference evaluatorthe definition of what a program means: demand-driven evaluation (eduction), traces, check mode, cachespackages/evaluator/src/evaluate.ts
Fast paths (TypeScript)group levels and fused kernels for values that recur together, for example training stepssrc/syntax/dense.ts, src/plan.ts
Plana versioned JSON description of the program: every equation, kernels, groups, sources, clocks, servicesplan format
Rust runtimeruns plans natively: kernels, full eduction, clocks and alignment, service calls, caches and memory management; the elucid8 command, a 1.8 MB binaryruntime/
Deploymenta plan, the elucid8 binary and a configuration file that binds sources to CSV or JSON-lines files, clocks and services; no Node at run timedeployment configuration
Servicesexternal programs that effects call, built in or as separate processesservices, protocol
Playgroundlearning and exploring in a browserthe Playground Tutorial

Two tiers

Exactness across languages holds because the plan fixes the operation order (for example, sums always add in the same order), and both languages use one maths library (fdlibm-5.3+exp1+msun-tanh).

How the stages fit

Each stage built on the last, and each was checked against what came before it.

stagewhat it addedresult
Milestones 1–5the language, the reference evaluator, the playground, training a networkexact, but up to 1,300× slower than plain TypeScript
Stage 2 (placement)placement derived from the equations, simulatedthe equations choose data parallelism by themselves
Stage 3 (hybrid runtime)group levels and fused kernels in TypeScripta training step from 3.6 s to 3.2 ms, near plain TypeScript
A1the plan, made explicitone contract for every engine
A2aRust runs kernels from plansfaster than TypeScript, 0.85 MB
A5one maths library in both languagesthe same bits on every machine and browser
A2b stage 1Rust evaluates any program by demand (eduction)about 200,000 outcomes identical to the reference
A2b stage 2clocks, alignment and service calls in Rustabout 680,000 outcomes and 200,000 calls identical
A2b stage 3source versions, caches across queries, memory managementidentical counts to the reference
A2c part 1the elucid8 command; CSV and JSON-lines connectors; npm run compilea million-row CSV loads in 0.09 s
Next: A2c part 2WebAssembly; what a long-running service needs
Then: F1, F2, pilotscommand-line tool, file and database connectors; AI model building and feature pipelines

Tutorial: one program, three ways

The files used here are in packages/evaluator/demo/guide/. You need Node.js. For parts 3 and 4 you also need Rust; build the elucid8 command once with cargo build --release from the repository root (it is then target/release/elucid8).

1. In the playground

Paste this program into the playground, give source x the table t,value with rows 0,3 · 1,1 · 2,4 · 3,0 · 4,5 · 5,9, and query running at t = 4:

-- A running sum, and a ratio that fails where x is 0.
dimension t : int range 0..5
source x @ (t)
running @ (t) = x fby.t (running + next.t x)
ratio @ (t) = running / x

You get 13 (3 + 1 + 4 + 0 + 5). running at t = 5 is 22, and ratio at t = 3 is Error: division by zero, because x is 0 there. Turn on trace to see every demand.

2. In the TypeScript reference

The same program, run from a script (demo/guide/reference.ts):

cd packages/evaluator
node demo/guide/reference.ts
running at t = 4: 13
running at t = 5: 22
ratio at t = 3: Error: division by zero

3. Compile a plan and run it with elucid8

Compile the plan. The compiler is TypeScript; the plan is plain JSON you can read:

cd packages/evaluator
npm run compile -- demo/guide/running.el8 -o /tmp/running-plan.json

Give it data. running-x.csv holds the same table as the playground (a header naming the source's dimensions and value, then one row per point):

t,value
0,3
1,1
2,4
3,0
4,5
5,9

Bind it. A deployment configuration (running.deploy.json) says where each source's data is and what to compute. It never holds credentials; paths are relative to the file (the format):

{ "format": "elucid8-deploy/1",
  "sources": { "x": { "csv": "running-x.csv" } },
  "queries": [ "running@{t = 4..5}", "ratio@{t = 3}" ] }

Run it. From the same directory:

../../target/release/elucid8 run /tmp/running-plan.json --config demo/guide/running.deploy.json
{"name":"running","context":{"t":4},"outcome":{"kind":"value","value":13}}
{"name":"running","context":{"t":5},"outcome":{"kind":"value","value":22}}
{"name":"ratio","context":{"t":3},"outcome":{"kind":"error","error":{"kind":"failed","message":"division by zero"}}}

One line per point, each outcome in the same JSON form the reference uses. A few variations:

Change the data. Edit running-x.csv so that t = 4 is 50, and run again: running at t = 5 becomes 67. Each file's version is a hash of its bytes, so changed data is a new version (the report shows it). A configuration may also list several files as versions of one source; with "options": { "pointCache": true }, a later query that pins another version recomputes only the points whose inputs changed (R3).

4. Add a clock and a service

alert.el8 reads x on a clock, sends it to a service with a deadline, and raises an alert:

dimension t : int clock sensor
source x @ (t)
effect total @ (t) =
  call sum_service(x, 1)
  deadline 20ms
alert @ (t) = total > 5

Its configuration (alert.deploy.json) reads x from JSON lines (alert-x.jsonl: {"t": 0, "value": 2} and {"t": 1, "value": 7}), the clock's timestamps from a CSV file (alert-sensor.csv: tick 0 at 0 ms, tick 1 at 100 ms), and binds the service:

{ "format": "elucid8-deploy/1",
  "sources": { "x": { "jsonl": "alert-x.jsonl" } },
  "clocks": { "sensor": { "csv": "alert-sensor.csv" } },
  "services": { "sum_service": { "builtin": "sum_service", "latencyMs": 5 } },
  "queries": [ "alert@{t = 0..1}" ] }

Compile and run as before (npm run compile -- demo/guide/alert.el8 -o /tmp/alert-plan.json, then elucid8 run /tmp/alert-plan.json --config demo/guide/alert.deploy.json --report /tmp/alert-report.json):

Change latencyMs to 30 and run again. Both answers become late errors, for example deadline 20 ms, completed 30 ms: the deadline counts from each tick's timestamp.

A service can also be a separate program that speaks a one-line-of-JSON protocol, holding its own credentials; the configuration names it ("process": ["node", "scorer.ts"]) and may name the environment variables it receives ("env": ["ANTHROPIC_API_KEY"]), never their values. See the protocol and the example in demo/services/.

5. Check Rust against the reference

The test suite runs every example, tutorial example and hundreds of random programs through both engines and requires identical outcomes:

npm test                       # from the repository root; includes the Rust comparisons when cargo is available
cargo test                     # Rust's own tests, against golden files the reference wrote

What is not ready yet