◆ Flux

Working in the editor

An editor for a general-purpose language spends most of its energy guessing: what does this name mean, what shape does this value have, what would happen if you ran it. An editor for Flux guesses far less, because the language hands it more to work with. Every value carries a kind, every program is a graph, and every evaluation is deterministic — the properties Guide §11 watched the compiler prove. So the tooling can filter a completion list by dimension, show you the value of a binding at the step under your cursor (the bar, in charting), and tell you why a signal is true — without running anything twice.

This chapter is a tour of what that tooling does, and, more usefully, why it can. Nothing below is a separate feature bolted on after the fact; each one falls out of a property the language already guarantees. When a capability seems to know something an editor “shouldn’t” be able to know cheaply, the reason is always the same: the graph is already typed and already computed, so the answer is a lookup rather than a run.

What the editor really edits is a view — the island the engine paints. A chart is the island that runs end-to-end today; the same typed graph and the same live loop reach a dashboard, a scene or a small application — the display and App planes those need are sealed design, rolling out after the chart. This documentation’s own editor is one such island, rendered live by the real engine: the strongest claim a language’s tooling can make is the one you can watch it make.

Code intelligence

Completion, filtered by kind

Type a . after a record and you get its members. Type it after a stream and you get only the functions whose first parameter accepts that kind:

close.          // → ema, sma, rsi, highest, … (everything that accepts a `price`)
rsi(close,14).  // → ema, sma, change, … (kind-preserving families) — but NOT `vwap`

The exclusions are the half worth reading. vwap wants a signal, so it is never offered after a price. Neither is atr — for a different reason: it reads high, low and close itself and takes only a length, so it has no source parameter for close. to fill. close.atr(14) is [ErrArg], and a list that offered it would be handing you a line that does not compile.

That is the payoff of method-style chaining: it turns the type system into a discovery mechanism. You do not need to know the catalogue; you need to know what you have. And at the head of an empty line, the editor offers the output verbs — plot, def, let, mark, alert — so a newcomer discovers that plot is how a value reaches the screen, instead of having to know it in advance.

Signature help and hover

Typing ema( opens ema(source: price, length: lit) → price, with the current argument highlighted.

A hover on an operation gives its documentation, its kind signature, a miniature example, and a live sparkline of that operation on the data currently on screen. A hover on a binding gives its inferred kind and its value at the step under the cursor.

That last one is worth pausing on: it is possible because evaluation is deterministic and the graph is already computed. There is no “debug build,” and nothing is re-run — the hover reads a value the runtime has in hand.

Diagnostics that teach

price + osc — you are adding a price and a 0–100 oscillator.
  close + rsi(close, 14)
          ^^^^^^^^^^^^^^ osc(0,100), a dimensionless bounded value
  Did you mean  close + atr(14)  (a price + a displacement)?

Three kinds of help sit behind that message:

Inlay hints

h = macd(close).hist                ⟦level · −12.3⟧
m = ema(close, 20) @ tf("1h")       ⟦price · @1h⟧

The ⟦…⟧ is not text in your file — it is rendered beside it. It shows the kind, the value at the cursor step, and — when a binding runs on a non-default clock — its clock provenance. You can see that a value comes from the hourly series, without the language having to encode the timeframe in the type, which would break the confluence idiom the whole design is built to allow.

The novice register

Warnings and style lints are deferred until your first green compile, then revealed opt-in. Day one never shows a wall of nags. The hard/soft classification of the error channel is unchanged — this is a presentation policy, not a semantic one.

The canonical formatter

Format on save, with no options to argue about — and one specific job beyond tidiness: it neutralizes the significant-newline trap. It normalizes line breaks and continuation indentation so the extent of every statement is visible. A newcomer never has to guess where an expression ended.

Semantic colouring by kind

Prices, oscillators, signals and canvas primitives are coloured differently from one another — not by syntactic category, but by what they are. It is a small thing that turns out to matter: you see the shape of a program’s dimensions before you read it.

Live preview

The editor compiles on idle, after a short debounce, and applies the result to the chart. When there is an error, it does not blank the preview: it evaluates the typable cone — the largest part of the graph whose every input is free of errors — and renders the rest as —. The last valid version is a fallback only if the cone is empty.

The consequence is worth stating plainly: a half-typed name costs you one value, never the screen.

The typable cone under a broken edit Figure — a single erroring node types to a contained hole; the cone of clean nodes still renders, and only the values downstream of the fault show as —.

Once the program compiles, the status chip tells you what it earned:

✓ No-repaint   ✓ No look-ahead   ✓ Deterministic   ✓ Bounded memory   ✓ Byte-identical
⚠ contains live() → non-replayable

Each tick is a guarantee the compiler proved, not a badge it assigned by convention — and each warning names the one construct that cost you one, so you always know what you traded and why.

For an application, hot reload generalizes: the host re-folds the retained message journal with the new update, without calling init — so your application’s state survives an edit. A change of shape falls back to the migration path; it is never a silent reset.

Debugging a graph

A Flux program has no call stack, so an imperative debugger would be answering a question nobody asked. What a program has is a graph of typed signals over an ordinal axis — so the cursor is two-dimensional: when (which bar) × what (which node). Every movement below is one way of reading that grid.

The two-dimensional debug cursor Figure — the debug cursor is a point on a grid of bar (when) against node (what); every tool below reads or moves that point.

Movement What it is
Ambient values the inlay hints already show every binding’s value at the cursor bar. There is no mode to enter.
The chart is the scrubber a playhead on the chart is the bar cursor — drag it, or use the arrow keys. No separate timeline.
The probe scrub to a bar and read the table of every binding’s value there. Deterministic, so it is exact rather than sampled.
The dataflow view the compiled graph, rendered as a graph — because that is what it is. Click a node: the source highlights and its series appears.
The causal cone “why is this signal true here?” — highlight everything the value at this bar actually depended on.
Data breakpoints not a line breakpoint but a data one: “go to the first bar where macd cross_up 0”. The series is already computed, so the jump is a search, not a re-run — it is instant. Likewise: the next event, the next na, the next divergence.
Time travel replay is exact, so stepping backwards is not an approximation; it is the same computation.

The through-line is that none of these re-runs your program. Because the series are already materialized and the computation is deterministic, “go to the next crossing” is a search over data you already have, and “step backwards” is the same numbers read in the other direction.

For an application, these same movements transpose to an (event, field) cursor over the message journal: the data breakpoint becomes “the first message where score crosses 40,” time travel becomes reverse-step along the journal, and the trust lens becomes a diff of the model against a reference run. The bar-axis debugger above is v1; its application twin lands with the App plane.

Sliders, and the parameter model

input(14, 2..200) renders a slider in the gutter. What happens when you drag it is the part worth knowing:

The tuned value lives in a parameter overlay, not in the source literal. The source carries the default. Dragging updates a parameter and re-runs the incremental step — it does not recompile, does not mutate your source, and does not flood your undo history.

The unit that is persisted, shared and replayed is therefore (source hash, parameters) — one compiled module, many tunings, with byte-identity on (artifact, parameters) → output. “Bake default” is an explicit action, and the only path that writes an overlay value back into the source.

A parameter with a declared range is bounded by its maximum: memory is sized for the worst case, so dragging a slider can never allocate. The work behind the slider is knowable before you touch it.

The performance HUD

Per script, the HUD reports the node count, the cost per bar, the canvas frame budget — and a warning when a script is heavy. This is compile-counted, bounded work, read off the graph rather than measured after the fact: the cost gutter reads the optimized graph, so what you see is what you pay.

Doc-as-data

The hover card, the completion list, this documentation, the error messages and the snippets all render from one structured record per function, kind, keyword and operator. They cannot drift, because they are the same data.

A completeness lint keeps it honest: every construct in the language has a documentation record and at least one runnable example — and every example is a golden. A documented function whose example stops working turns a test red, which is the only kind of documentation guarantee worth having.

See also

The formal rules → Everything above is taught intuition; the normative statements live in the spec, the FVM and the FDK.