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.
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:
- Guaranteed termination. No script hangs, ever — yours or anyone else’s.
- A static budget. Memory and cost-per-unit are computed at compile time; a program that
exceeds the budget is rejected before it runs with
[ErrTotal], the offending bound named — never killed mid-run by a timeout. - Sandbox by construction. A total program with no I/O primitives cannot be turned into a resource-exhaustion attack. This is the foundation the trust tiers stand on.
- Verifiability everywhere else. The static checks — kinds, causality, exhaustiveness, the optimizer’s oracle — are decidable because the language is total and its bounds are constants.
Iteration still exists; it is just honest about its ceiling. Here are the three shapes it takes:
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:
- Delays are past-only.
x[n]reachesnsteps back, withn ≥ 0a constant. A negative delay does not parse into a meaning; it raises[ErrCausal]. - Resampling reads only closed units.
x @ "1d"reads the last closed daily unit — never the one still forming, never a future close. (The forming unit is reachable for display only, throughlive(), which the firewall keeps out of every analysis.) - Feedback must cross a unit delay. Any cycle in the graph must pass through
x[1]— the rulescanobeys internally — so today’s output may depend on yesterday’s output, never on itself.
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:
- Live ≡ historical. The stream you compute live and the stream you replay over history are the same function — the honest backbone of any evaluation.
- Trustable signals. A crossing that fired stays fired. Marks and alerts stand on ground that cannot shift.
- Machine-checkable. Causality is a graph property — no zero-delay cycles — decided at compile time, the same clock-calculus idea proven in synchronous languages.
In practice, the legal and the illegal sit one line apart:
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 futureThe 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:
- Scalar
f64, no SIMD in the deterministic domain, no FP reassociation. Vector reordering and re-associated reductions change bits; the deterministic core refuses both, and reduction order is fixed. - Transcendentals —
log,exp,sin,cos,tan,atan,atan2,pow— route through one pinned libm build, never the host engine’sMath, which legitimately differs by an ULP between engines. - Decimal, Unicode, calendar, PRNG. Fixed-point money math, string operations (Unicode scalar units, pinned case tables), calendar arithmetic (pinned time-zone data) and the seeded random generator (a counter-based integer PRNG) are each one shared routine, used identically by the interpreter, the compiled module and any re-executor.
- A canonical
na. Missing values compare as absent everywhere, and at every storage or hashing boundarynais forced to a single bit pattern — so two engines never disagree on the bytes of “nothing”.
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:
- Replay-exact debugging. Step backwards as reliably as forwards; every value is reproducible on demand.
- Tests that mean something. A golden snapshot either matches exactly or the program changed — no tolerance to tune, no flakiness to excuse.
- Cross-machine agreement. Two devices — or a client and a verifying server — compute identical results, which is what makes shared artifacts and independent verification honest.
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 analysisRandomness, 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:
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 meaningWhat that gives you:
- Nonsense is rejected at compile time, with a dimensional explanation and a suggested fix — not
a mystery
NaNthree steps downstream. - Presentation is inferred. The kind is rich enough to derive display: a
priceoverlays the price axis; anosc(0,100)gets its own pane, a fixed scale and its guide lines; asignalrenders as marks. One line of mathematics materializes as a correctly furnished chart because the kind already said everything needed. - Inference is silent until you are wrong. You annotate nothing; kinds flow bottom-up from sources through operators, every expression gets its most precise kind, and the only time you hear from the system is when it saves 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:
- Guarantees that survive decoration. An animated, randomized effect can sit pixels away from a signal with no possibility of contaminating it. No-repaint holds even in programs full of animation, because the dependency cannot be expressed.
- Freedom where it is safe. CANVAS authors get wall-time, easing, springs, noise and interactivity with no determinism tax — the plane is honestly outside the guarantees, and the firewall is what makes that honesty affordable.
- A clean audit surface. Whether a program is replayable is a static fact about which plane its sinks live in, and the compiler tells you.
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 ANALYSISThe 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:
- Commands are inert data. A
Cmdcarries values — a sound name, a storage key, a score — never a token, URL or handle. The host holds every resource and executes the command only if the matching capability was declared and granted. Emitting a command the manifest does not grant is rejected at compile time ([ErrCapDenied]); it never becomes a runtime event to catch. - Capabilities are declarations, not values. They are named
namespace:verbentries in the app descriptor —chart:read,storage:own,net:fetch— never first-class objects, so they cannot be re-delegated or amplified from inside a script. - Manifests aggregate transitively, with zero escalation. An artifact’s sealed manifest is the union of capability needs over its whole dependency closure, intersected with the user’s grant. A dependency’s need surfaces in the top-level manifest before install, and no dependency can ever hold a capability the user did not grant to the whole.
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:
- Untrusted code as a routine. Installing an artifact is informed consent to a short, exact list — not an act of faith in an author. Author-untrusted code is the general case; AI-generated code is one legitimate instance the same guarantees cover.
- No confused deputies. Authority flows only along import edges, capped by the grant; a library cannot launder access through the app that embeds it.
- First-party honesty. The platform’s own applications run under the same regime with the same manifests — trust is a grant level, never a code path.
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:
- Free sharing. Common-subexpression elimination is global: write
ema(close, 26)in four places within a script and it is computed once. Sharing it across four co-active scripts — one merged graph for the whole chart — is a later optimizer tier. - Cost you can see. Dead code is eliminated, constants folded, element-wise chains fused; the editor’s cost gutter shows the optimized graph, so what you read is what you pay.
- A safe default and a labeled fast lane. The default is bit-exact, always. Relaxed floating-point (reassociation, fused multiply-add) is designed as an explicit, labelled opt-in — never a silent default, and never bit-exact.
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-exactThe 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:
- Totality makes verification possible. Every static analysis in the language — kind inference, causality checking, exhaustiveness, the optimizer’s validation oracle — terminates because programs are total and their bounds are constants. A language with unbounded programs could promise none of its checks complete.
- Causality makes the graph a DAG, which is what kind inference walks in one pass, the scheduler parallelizes without locks, the debugger checkpoints and replays, and the optimizer reorders safely. One acyclicity theorem, consumed four ways.
- Determinism needs totality and causality — replay is only exact if programs terminate and history is immutable — and needs the firewall, which keeps wall-time and randomness out of the deterministic domain instead of asking authors to be careful.
- Kinds feed everything. Presentation inference (pillar 4), the memory plan that makes budgets static (pillar 1), the firewall’s classification of sinks (pillar 5) and the optimizer’s cost model (pillar 7) all read the same dimensional facts.
- Capability security stands on totality and purity. Default-deny would be theater if a script could loop forever, reach ambient I/O, or hide effects in evaluation order. Because it can do none of these, the capability manifest really is the complete story of what an artifact can do.
- The optimizer’s freedom is determinism’s dividend. “Bit-identical to the reference” is only a usable law because a reference answer exists — deterministic, byte-stable, on every machine. In exchange, the optimizer repays the budget that totality promised: the cost the compiler declared is the cost you observe.
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
- What is Flux? — the mental model and the four planes at a glance.
- Your first session — the pillars as you experience them while writing.
- The four planes — the firewall in depth.
- What Flux deliberately doesn’t do — the honest edges of “total, not Turing-complete”.
- Determinism, replay and trust — each promise and the machine check that enforces it.
The formal rules →
- Totality & bounded iteration (pillar 1) — Time & state → Windows and bounded iteration.
- Causality & no-repaint (pillar 2) — Time & state → Causality is a theorem and the delay operator
x[n].- Byte-determinism & I7 (pillar 3) — Verification & reproducible builds.
- Dimensional kinds (pillar 4) — Kinds → The affine substrate and the operator algebra.
- Planes & the firewall (pillar 5) — The App plane → Composition and the firewall.
- Capability security (pillar 6) — The App plane → Capabilities and the host-services capability catalogue.
- The verified optimizer (pillar 7) — The optimizer → The law and The tiers.