noise~lang

The language guide

This page is a skill — install it into your coding agent (Claude Code, Cursor, Codex, Copilot, Windsurf…) with one command. Click to copy.

Writing Noise

Noise is a small, expression-based probabilistic language: every value is a probability distribution, and you compute probabilities and expectations by Monte Carlo. A program is a sequence of ;-separated statements and its result is the value of the last statement. This guide is self-contained — everything you need to write correct, idiomatic Noise is below.

Run it

cargo run -p noise-cli -- file.noise   # run a program; prints the LAST statement's value
cargo run -p noise-cli                 # REPL (one line at a time, persistent env)

(If a noise binary is installed, noise file.noise / noise work the same way.)

The mental model

Four load-bearing rules. Internalize these and the rest follows.

  1. Everything is a distribution. A number is just the degenerate case — a point mass (mean(5)=5, variance(5)=0, P(5>3)=1). Operators map distributions to distributions uniformly, so P, E, Var, Q apply to anything.

  2. ~ draws; = transforms. name ~ dist is a stochastic node — it draws a fresh random variable, and ~ is the only thing that draws. name = expr is a deterministic node — a transform, a constant, or an undrawn recipe. You cannot do arithmetic on an undrawn recipe:

    unif(0, 1) + 3        # ERROR — unif(...) is a recipe, not a number
    X ~ unif(0, 1); X + 3 # correct — draw with ~, then transform with =

    unif, unif_int, bernoulli, normal, … all return an undrawn recipe. Bind a recipe with = to name it (Die = unif_int(1, 6)), then draw it with ~ (a ~ Die).

  3. One name = one fixed node. Every mention of a name is the same draw, exactly like X in math. So X + X is 2X, X - X is exactly 0, and P(X == 6 && X > 3) uses one X. There is no re-draw on reuse.

  4. Independence is explicit. Two independent draws come from two ~ declarations, or from the shaped draw ~[n] dist — never from repeating a name.

    A ~ unif_int(1, 6); B ~ unif_int(1, 6)   # two independent dice
    dice ~[2] unif_int(1, 6)                  # same thing, as a length-2 array

Nothing is sampled until a query (P/E/Var/Q) forces it; everything upstream stays symbolic.

Language reference

Lexical. Whitespace is insignificant (separates tokens). Comments run to end of line, started by # or //. Numbers are f64 integer or decimal literals — no exponent syntax, and a leading - is the unary-minus operator, not part of the literal. Identifiers are [A-Za-z_][A-Za-z0-9_]*, case-sensitive. Reserved words: if else for in use true false. Strings are double-quoted with no escape sequences ("like this"); they are a label/utility type for Print and messages — a string can never enter a random-variable expression.

Operators (precedence low → high; all left-associative except ** and binding, which are right-associative; prefix -/! bind tighter than everything below **, so -2 ** 2 == -4):

LevelOperatorsMeaning
1= ~binding (right-assoc)
2..half-open integer range [a, b)
3||logical or
4&&logical and
5== != < > <= >=comparison
6+ -add / subtract (+ also concatenates if either side is a string)
7* / @multiply / divide (elementwise/broadcast) · @ = matrix product
8**power (right-assoc)
9prefix - ! ~negate · logical not · draw
10postfix [index]index (repeatable: M[i][j])
11call, () grouping, {} blocks

Types & rules (every value is conceptually a distribution; these are the runtime forms): number (point mass), bool (point mass on {T,F}), string, unit (), array (fixed-length, known at build time), dist (a drawn random variable / distribution handle), estimate (a number carrying a standard error, produced by P/E/Var), and signal (a lazy waveform).

Modules & use

builtin is always active: P, Q, E, Var, Print, Len (capitalized). Everything else is strict — a bare name errors until you use its module (or write the mod::name path). Start each program with the use lines you need.

Moduleuse?Items
builtinalwaysP, Q, E, Var, Print, Len
randuse rand;unif, unif_int, bernoulli, normal, normal_int, exp, exp_int, poisson, geometric, rotation
mathuse math;pi, e, sqrt, round, log (natural), log10, sin, cos, atan, sign
vecuse vec;sum, count, any, all, max, min, mean, dot, normsq, norm, vadd, vsub, matvec, transpose, normalize, quantize, has_duplicates, count_duplicates, mse, ones, zeros, iota
signaluse signal;sine, cosine, noise_white, sample
use rand;            # unif, unif_int, …
math::sqrt(2)        # or reach one item by its full path, no `use` needed

A user definition shadows a module item of the same name. Module paths are single-level (mod::name); a constant path resolves a value (math::pi), a function path must be called.

The standard workflow

recipe (=) → draw (~ / ~[n]) → transform (=) → query (P/E/Var/Q) → Print.

use rand;   # unif_int
use vec;    # has_duplicates

n     = 23;
bday  = unif_int(1, 365);   # a recipe
days  ~[n] bday;            # n independent draws
match = has_duplicates(days);
Print("P(shared birthday among", n, ") =", P(match))

Distributions & queries

Distributions (all rand, all return recipes; draw with ~):

Queries (all builtin; default n = 1e6 samples, fixed seed → reproducible):

Collections, arrays & idioms

Arrays are fixed-length and known at build time. Build them with literals ([1, 2, 3], []), the range a..b (half-open: 0..n is 0 … n-1), the shaped draw ~[n] d, or vec constructors (ones(n), zeros(n), iota(n)). Index with xs[i] (chains: M[i][j]); the index must be a constant non-negative integer in range — never a random variable. There is no append/push.

Arithmetic broadcasts over arrays (NumPy-style, nesting for matrices): [1,2,3] + [10,20,30][11,22,33], 1 + [1,2,3][2,3,4], [1,2,3] ** 2[1,4,9]. The @ operator is the matrix product (v @ w dot, M @ v matvec, M @ N matmul); * stays elementwise. sin/cos/atan are ufuncs (scalar, lifted over RVs, or mapped over arrays).

Shaped draws & reducers. ~[n] d is n iid draws (an array); ~[n, m] d a matrix. Fold with a vec reducer — independence becomes a one-liner:

use rand; use vec;
dice  ~[2] unif_int(1, 6); Print("P(sum==7) =", P(sum(dice) == 7))      # two dice
flips ~[3] bernoulli(0.5); Print("P(all heads) =", P(all(flips)))       # 3-coin streak
flips ~[3] bernoulli(0.5); Print("P(exactly 2) =", P(count(flips)==2))  # count of true
parts ~[3] bernoulli(0.9); Print("uptime =", P(any(parts)))             # at-least-one

Lifted if = per-lane select (a value, not control flow; else required, both branches evaluated and reuse the condition’s per-lane draws). Gives max/min/abs over RVs for free:

A ~ unif_int(1, 6); B ~ unif_int(1, 6);
higher = if A > B { A } else { B };     # max of two dice
Print("P(higher==6) =", P(higher == 6))

Ranges & for (build-time unroll). for x in xs { } runs the body once per element; bindings leak (blocks don’t scope) — that’s exactly how an accumulator persists. Each ~ inside the body is a distinct node, so it’s a clean way to make many independent draws:

use vec;
acc = 0; for x in 1..5 { acc = acc + x }; acc      # 1+2+3+4 = 10

User functions. f(a) = expr is pure (lifts over RVs); f() ~ dist draws fresh per call:

use rand;
max(a, b) = if a > b { a } else { b };   # pure
roll() ~ unif_int(1, 6);                 # fresh draw each call
P(roll() + roll() == 7)                  # two INDEPENDENT rolls

Functions are pure in their parameters — the body sees only its args (plus pi/e), no outer variables, no closures. Calls unroll at build time, so recursion must terminate.

Conditional probability has no native observe; write it as a ratio:

use rand;
D ~ unif_int(1, 6);
Print("P(D==6 | D>3) =", P(D == 6 && D > 3) / P(D > 3))   # = 1/3

Signals (lazy waveforms). signal::sine(f) / cosine(f) describe a waveform by frequency (O(1) memory). Scalar/trig ops defer into the signal; it materializes to an array when it meets a sized array (adopting its length) or via signal::sample(sig, n). noise_white(sigma) is a lazy random generator (materializes to fresh iid normal(0, sigma) draws). sine(n, f) is shorthand for sample(sine(f), n).

use signal;
msg = sample(0.3 * sine(3), 64);   # render the lazy tone at 64 points

Hazards — what NOT to do

Style conventions

Pre-flight checklist