Keep scrolling
The best way to learn a language is to watch it unfold in front of you — so keep scrolling. Six small programs, each one idea past the last, animate as you read them, then run for real on the compiled engine in your browser. It has a touch of magic to it: every variable here is a whole wave of possibility, and a query is what collapses it to a single number.
Examples
A catalogue of short programs, each a Monte-Carlo experiment with a known closed form, so the printed answer can be checked. Open any one in the playground to run and edit it — the real Noise compiler, built to WebAssembly and running in your browser. Each program gets its own shareable link.
Basics
Probability
Games & risk
Continuous & CLT
Signals & DSP
Functions & research
How it works
Noise is small by design. Everything above is built from a handful of ideas.
Everything is a distribution
A number is just a distribution with all its weight on a single point — a Dirac delta. Operators lift over random variables automatically — so X below is random, and Y is random too, with no special syntax. Propagating uncertainty reads exactly like ordinary arithmetic.
X ~ unif(-1, 1)
Y = 2 * X + 3 # Y is a distribution too The tilde draws; equals transforms
A name bound with ~ is one fixed random draw that every mention reuses — so X − X is exactly 0, never "two samples." Independence comes from separate ~ bindings, exactly like writing X₁, X₂ on paper. No hidden re-draws, no surprises.
A ~ unif_int(1, 6)
B ~ unif_int(1, 6) # two independent dice
A + B # a genuine 2d6 distribution Lazy until queried: P, E, Var, Q
Nothing is sampled until you ask. A query runs a fast columnar Monte-Carlo pass and reports an honest estimate — the printed digits reflect the standard error, and that error propagates through arithmetic, so 4·P(C) rounds itself correctly.
C = X^2 + Y^2 < 1
4 * P(C) # ≈ 3.14 Independence is a shape
Put a shape on the tilde to draw a whole batch at once: ~[n] is an iid vector, ~[n, m] a matrix. A reducer collapses it back to one number — so the birthday paradox over 23 people, all 253 pairwise comparisons, is a single expression.
days ~[23] unif_int(1, 365)
P(has_duplicates(days)) # ≈ 0.51 Branches are random variables
When the condition is random, if c { a } else { b } does not take a path — it builds a new random variable, choosing a or b per sample. That single rule hands you max, min, abs, clamps and payoffs over distributions for free.
RandomVariableToo = if A > B { A } else { B } # the larger of two dice Performance
Almost every Noise program ends in “evaluate this expression over a few million random
draws.” That loop is compiled, not interpreted. ~ and the
distribution constructors build a graph IR that lowers three ways — a portable columnar
interpreter, a native JIT via Cranelift, and a
WebAssembly emitter for the browser — all sharing one cost model, so the backend only ever
changes speed, never results (bit-identical across core counts).
You write a one-line P(...) and get an expert kernel for
free. It is built from a stack of techniques, each with its own measured win:
- Kernel fusion — the codegen backends emit one loop that draws its sources, computes the whole expression in registers, and stores only the result, erasing the interpreter's intermediate memory traffic.
- Graph simplification — constant folding, finite-safe algebraic identities, and
common-subexpression elimination shrink the DAG before any code is generated (so
X + Xis one draw, not two). - Inlined xoshiro256++ PRNG — the generator is emitted straight into the kernel as a handful of shifts/xors/rotates, with zero call overhead on native and in WASM alike.
- Inlined transcendentals —
ln/sin/cos(the heart ofnormal,exp, and signals) become straight-line polynomial approximations (~1e-9 vs libm), roughly doubling transcendental-bound kernels and skipping a per-draw crossing of the JS boundary in the browser. - Multi-stream RNG — four independent xoshiro streams run at once to hide the generator's serial-dependency latency (the scalar form of SIMD), switched on only where the graph is latency-bound.
- Columnar batches — the interpreter runs 1024 lanes through one instruction at a
time: a tight, cache-friendly, auto-vectorizing pass over contiguous
f64s. - Vectorized power-sum reduction — moments accumulate as raw power sums across eight unrolled lanes with no per-element divide: ~9.5× faster than a streaming Welford update, turning the reduction from the ceiling into a rounding error.
- Deterministic multicore — sampling fans out with a work-stealing loop whose per-chunk accumulators merge as an exactly-associative monoid, so the answer is bit-identical regardless of thread count, and reproducible from a seed.
- Profitability gate — a cost model emits a fused kernel only where it beats the vectorized interpreter, so codegen can change the speed but never lose.
The payoff, measured on a 14-core M4 Pro:
- ~5.8 billion samples/sec (π Monte Carlo, generate + reduce, all cores), scaling ~9.6× from one core to all of them.
- Within ~1.15× of hand-written, LLVM-compiled Rust per core — and faster end to end, because the one-liner fuses and fans out across every core with no flags or annotations.
- In the browser the emitted WASM kernel runs the same fused loop at ~0.5–0.75× of native codegen — hundreds of millions of samples/sec, client-side.
The full write-up, with the benchmark tables behind each number, is in PERF.md.