◆ Flux

Why Flux is built this way

Most languages hand you a general-purpose engine and trust you to stay out of trouble: don’t read the future, don’t add a price to an oscillator, don’t ship code that hangs the tab, don’t let a plugin phone home. Flux takes the opposite bet. It makes the trouble inexpressible — so the guarantees you care about hold for every program the compiler accepts, not for the careful subset you managed to write.

Seven pillars carry that bet. Each one is a property enforced by construction — not a convention, not a linter rule, not a best practice, but something the compiler proves about every accepted program and, where proof needs a hand, verifies by machine at every compilation. This chapter walks each pillar in turn: what it says, why it exists, what it buys you, and one small program that shows it working. Read it to understand why Flux is shaped the way it is; read Your first session to feel the same shape while you type.

The seven pillars Figure — seven pillars supporting one guarantee set: bounded cost, immutable history, identical bytes on every engine, inferred presentation, contained effects, and an optimizer you need not trust.

1. Total, not Turing-complete

Every Flux program terminates, and the cost of one step is known at compile time. There are no unbounded loops, no unbounded recursion, no unbounded collections.

That is a choice, and Flux makes it openly. Flux belongs to the synchronous-dataflow family — the lineage of Lustre and SCADE — languages built for systems where “the program might not finish this step” is not an acceptable outcome. A charting host has to advance every active script on every data unit inside a frame budget; a platform that runs other people’s code has to bound what that code can consume. Turing-completeness would remove exactly these guarantees, because the halting problem makes an unbounded script impossible to budget — and it buys nothing that real analytical programs need. The model is unbounded over time, bounded per step: a stream runs forever, but the work inside each step is finite and statically known.

What that gives you:

Iteration still exists; it is just honest about its ceiling. Here are the three shapes it takes:

FLUX
hi20 = highest(close, 20)                        // windowed reducer — the bound is a constant
w    = window(close, 20)                         // the same 20 values as a vec, for map/fold
hi   = scan(close, (prev) -> math.max(prev, close))   // running state with feedback (see pillar 2)

window with map/fold covers the counted loop, scan covers running state, and loop(max, …) covers “iterate until done” with a declared ceiling. Every bound is a compile-time constant under a global cap; a data-dependent length is a kind error, not a runtime surprise. The limit only bites on unbounded inner computation — which, in a client-side reactive language, is a hung tab, i.e. a bug you never wanted. What Flux deliberately doesn’t do walks the honest edges.

2. Causal by construction

A value produced for a step can never change afterwards. History is immutable — not as a discipline you maintain, but as a theorem of the language.

In any system that re-evaluates over growing history, the deadliest defect is the value that quietly changes retroactively: an analytic that looked prophetic on the past because, at each past step, it had silently read data that did not exist yet. The result is a live/replay divergence no test catches, because both runs are “correct” — for different definitions of time. Flux removes the defect at the root by making it impossible to write.

Three rules produce the theorem:

By induction over the graph, output[t] = f(inputs[0..t]), mathematically. A past value has nothing left to depend on, so nothing can move it. This is no-repaint — a value, once produced for a step, never changes — and it holds for every accepted program.

What that gives you:

In practice, the legal and the illegal sit one line apart:

FLUX
prev  = close[1]                 // yesterday's close — legal, and na on the first bar
gain  = math.max(close - prev, 0)
peek  = close[-1]                // ✗ [ErrCausal] — a negative delay reads the future

The rejection arrives with its reason: a value from the future would force your past values to change once reality caught up.

3. Deterministic to the byte

The same program on the same data produces the same bytes — between the editing interpreter and the compiled WebAssembly module, between two runs, between two machines.

“Roughly equal” is not a property you can build on; byte-equality is. It makes replay exact, golden tests meaningful, results reproducible across devices, and independent re-execution — a server re-checking a client’s run — possible at all. Floating point is deterministic if and only if every source of variance is pinned, so Flux pins all of them, as language policy rather than author burden:

The equivalence is not assumed; it is verified at every compilation by running the interpreter and the compiled module on real data and asserting bit-equality — invariant I7. A divergence blocks the artifact. Note the scope: this is bit-identical computation and, downstream, a deterministic scene — the same numbers and the same scene on every machine, not the same pixels on every GPU.

What that gives you:

FLUX
r = math.log(close / close[1])        // ratio in, dimensionless out — via the pinned libm, same bits everywhere
dot { at: (bar.i, close), glow: 8 * rand(42) }   // rand: a presentation generator — legal here, walled out of analysis

Randomness, seeded or not, lives on the presentation side of the firewall (pillar 5), where determinism is deliberately not promised — analysis code that tries to read rand is stopped with [ErrFirewall].

4. Dimensional kinds

Every stream carries a kind: a dimensional type that records what the value is physically, not merely that it is a number. The kind system computes the kind of every result and rejects operations that have no physical meaning.

In analytical code, the worst bugs are not the type errors a conventional checker would catch — everything is a float. They are dimension errors: adding a price to an oscillator, comparing a volume to a ratio, feeding a percentage where a level is expected. All of these are well-typed nonsense in a float-only world. Flux gives data its physics back. The price axis is modeled as an affine space: price is a point, level is a displacement (a vector), ratio is a dimensionless scalar, and the dimensions form an algebra under the operators. The pleasant results are theorems, not special cases:

FLUX
range = high - low                                //   price − price → level  (point − point = vector)
band  = sma(close, 20) + 2 * stdev(close, 20)     //   price + level → price  (point + vector = point)
rel   = close / open                              //   price ÷ price → ratio  (dimensionless)
bad   = close + rsi(close, 14)                    // ✗ [ErrDim] — point + dimensionless: no affine meaning

What that gives you:

Kinds are the keystone the other pillars lean on: the memory planner sizes buffers from kinds, the optimizer’s cost model reads them, and the editor’s completion filters by them. The full system — sorts, the lattice, coercion, the operator algebra — is specified in Kinds.

5. Planes with a one-way firewall

Computation and presentation are separate planes, and dependence crosses between them in exactly one direction: presentation may read analysis; analysis may never read presentation. Violations are compile-time errors — [ErrFirewall].

A language that promises determinism and wants delightful, animated, interactive output has a problem: screens, wall clocks, pointers and randomness are exactly the things determinism must exclude. The usual outcomes are grim — either the guarantees quietly erode into “mostly deterministic”, or the output layer is starved into lifelessness. Flux refuses the dilemma structurally. Everything non-deterministic — now(), screen coordinates, hover state, unseeded rand/noise — exists, but only on the presentation side (CANVAS, TRANSITION), where nothing downstream depends on exactness. The ANALYSIS and APP planes stay inside the guarantees. The wall between them is directional and compiler-enforced.

What that gives you:

FLUX
dot { at:(bar.i, close); r: 4 }       // CANVAS reading an ANALYSIS value — the legal direction
x = ema(now(), 20)                    // ✗ [ErrFirewall] — wall-clock time cannot enter ANALYSIS

The same rule gives live(e) — the presentation-side read of the unit still in formation — a safe home: it may flow to display sinks, never into confirmed analysis. The four planes covers the firewall in depth.

6. Capability security

Scripts have no ambient authority. Effects are default-deny object-capabilities: a script can affect the world only through capabilities it declared, that the user granted, and that the host mediates.

Flux is the language of a platform where code is shared, sold and run by people who did not write it — and that is only tenable if safety is a property of the language and host, not of a review process. Flux gets there in layers. The language has no primitive that does I/O — no fetch, no DOM access, no eval, no file handles — so a script’s only channel to the world is data it hands the host. On the APP plane that channel is explicit:

FLUX
app quiz {
  capabilities: [ chart:read, sfx, storage:own ]   // everything this app may ever touch
  // ... a Cmd like PlaySfx("ding") is data; the host decides whether it runs
}

What that gives you:

7. Optimizable by construction

A Flux program is a pure, typed, total, causal DAG — the form on which classic optimizations are safe by construction. And the optimizer is verified at every compilation, never trusted.

In impure languages, optimizers spend their sophistication proving that a transformation cannot observe an effect, then give up conservatively when they cannot. Flux programs have no effects to observe: any two computations of the same pure subgraph are interchangeable, so sharing, pruning, reordering and specializing need no heroics. The program’s small, bounded size adds a second, unusual advantage — whole-graph searches that are infeasible on large programs are affordable here, so the optimizer can aim for optimal rather than “good enough”.

The trust model is the distinctive part. The optimizer obeys one law: the optimized program must be bit-identical to the reference semantics — the unoptimized DAG’s canonical evaluation, or the native kernel for built-ins. That law is enforced by translation validation: every compilation runs reference and optimized artifacts on real data and asserts byte-equality. The optimizer may therefore be aggressive precisely because no one has to believe in it — a miscompilation cannot ship, it can only fail loudly at the gate.

What that gives you:

FLUX
def ema0(s, n) = let a = 2/(n+1) in scan(s, (p) -> a*s + (1-a)*p)
def macd0(s)   = let l = ema0(s, 12) - ema0(s, 26) in { macd: l, signal: ema0(l, 9), hist: l - ema0(l, 9) }

plot macd0(close).macd, macd0(close).signal    // two calls, one shared subgraph — CSE, verified bit-exact

The tiers, the cost model and the validation harness are specified in The optimizer.

How the pillars compose

The pillars are not seven independent features. Each one leans on the others, and the guarantee set holds because the loop closes:

Pull any one pillar out and the others weaken; together they close. Describe an expression, and the engine can evaluate it — provably terminating, never rewriting history, identical everywhere, disclosing everything it touches, at a cost known in advance.

See also

The formal rules →