noise~lang — a probabilistic programming language

6 min read Original article ↗
Figure 1. the noise field this language is named for — a fractal-noise surface drawn as ink contours. Move your cursor to disturb it.

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 + X is 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 transcendentalsln/sin/cos (the heart of normal, 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.

About the creator

Manu Mtz.-Almeida. Creator of Gin, core contributor to Ionic, Stencil and Qwik. Principal engineer at Builder.io, working on compilers, high-performance systems, and AI agents.

I started Noise nine years ago and never quite finished it. The idea grew out of my telecommunications degree — a world of signals, noise, and probability — where I kept wishing for a language that could express uncertainty as naturally as it expresses arithmetic. This is that wish.

github.com/manucorporat · x.com/manucorporat · linkedin