Probl

A language for probability models.

Probl is a probabilistic programming language for probabilities, simulations and forecasts. Write models with variables, functions and loops. It reports distributions over their possible outcomes. Use it to calculate game odds, compare decisions, forecast uncertain quantities or update a model with evidence.

The playground runs models locally in your browser using WebAssembly. No installation is required.

craps.problOpen in the playground
# Craps, pass line bet: what's the chance of winning?let come_out ~ 2d6               # one world per totalvar win = falseif come_out in [7, 11] {  win = true} else if come_out not in [2, 3, 12] {  loop {                         # keep rolling for the point    let r ~ 2d6    if r == come_out { win = true; break }    if r == 7 { break }  }}report win
enumeratedwin    49.29%

This craps model has no fixed number of rolls. Probl solves its 6 repeating states as a Markov chain, giving 49.29%. The mathematical probability is 244/495; the engine uses floating-point arithmetic.

How it works

Enumeration follows weighted outcomes

In enumeration mode, a world is a program state with a weight. Random choices split worlds, equivalent states merge, and reports summarize the remaining population. Boolean conditions follow the matching branch; probability conditions explore both.

  1. 1 Random choices create worlds

    if 30% gives its two branches weights of 0.3 and 0.7. A discrete draw splits too: let r ~ 2d6 gives eleven possible totals, each with its probability.

    weather.problOpen in the playground
    let weather = if 30% { "rain" } else { "sun" }report weather
    "rain"0.3
    "sun"0.7
  2. 2 Equal worlds merge

    Worlds with the same state needed by later code combine, adding their weights. Here two paths reach 0 after two steps and combine into one world with weight ½.

    A thousand coin-flip steps have 21000 paths but only 1,001 final positions. Keeping only the current position lets those paths merge; storing the whole history would prevent that.

    walk.problOpen in the playground
    var pos = 0repeat 3 { pos += if 50% { 1 } else { -1 } }report pos
    0 1 −1 ½ +1 ½ −2 ¼ 0 ½ +2 ¼ −3 ⅛ −1 ⅜ +1 ⅜ +3 ⅛
  3. 3 Reports summarize the results

    report collects values across worlds. It displays probabilities for events and summaries for numeric quantities. Ordinary expressions operate within each world; a report performs the aggregation.

    weather.problOpen in the playground
    enumeratedweather    sun 70.00% · rain 30.00%

Games

Game probabilities from ordinary code

Dice, card draws and turn-based rules can be written directly. Merging equivalent states keeps many models manageable, although models that retain many distinct histories can still grow exponentially.

Ten dice, rolled one at a time
60,466,176 possible roll sequences, with 51 totals. This model peaks at 276 worlds.
A Risk battle, fought to the end
Up to 56 × 21 distinct dice outcomes per round. After a round, only the remaining army counts matter, so equivalent worlds can merge.
Loops with no fixed end
Some unbounded loops stop when the remaining weight falls below a tolerance, 10−12 by default. Reports identify unresolved mass. Resource limits can also stop a run.
04_risk_battle.problOpen in the playground
# Risk: 10 attacking armies against 8 defenders, fought to the end.@mode enumeratevar attackers = 10var defenders = 8while attackers > 1 and defenders > 0 {  # The attacker rolls up to three dice but must leave one army at home;  # the defender rolls up to two. roll(n, d6) gives the dice sorted high to low.  let attack ~ roll(min(3, attackers - 1), d6)  let defend ~ roll(min(2, defenders), d6)  # Highest against highest, second against second. Ties go to the defender.  for i in 0..<min(attack.len(), defend.len()) {    if attack[i] > defend[i] { defenders -= 1 } else { attackers -= 1 }  }}# A full round has 56 × 21 distinct dice outcomes. But once it's over, the# dice are never read again, so the only state left is the two army counts:# the worlds merge back into a handful after every round.report defenders == 0 as "attacker conquers"report attackers - 1 as "armies left to move in"
enumeratedattacker conquers         64.64%armies left to move in    mean 3.05 · sd 2.78 · 5% 0 · median 3 · 95% 8 · 0 █▁▂▃▃▃▃▂▂▁ 9

Forecasting

Forecast distributions over time

Select sampling for large state spaces or continuous calculations that enumeration does not support. Each run follows one random path, and reports include sampling uncertainty. Enumeration is the default; it does not automatically switch modes.

Monthly recurring revenue after a launch, while the market drifts between boom, steady and slump
$0 $50k $100k $150k $200k 1 6 12 18 month $100k a month

90% of runs 50% median

  • 3% to 7% specifies a lognormal distribution whose 5th and 95th percentiles are 3% and 7%.
  • normal_range(-8%, 0%) uses a normal distribution with those percentiles, allowing negative values.
  • report … by month produces a table of monthly distributions. This chart plots its median and central intervals.
sample · 50,000 runs · seed 11MRR ≥ $100k after 18 months    33.3% ± 0.2%
07_launch_forecast.probl (part)Open in the playground
let price = 49                 # $ per customer per monthlet churn ~ 3% to 7%           # share of customers who leave each month: uncertain, but fixedvar market ~ one_of([Boom: 20%, Steady: 60%, Slump: 20%])var signups ~ 60 to 150        # new customers per month at launchvar customers = 0for month in 1..18 {  let new ~ poisson(signups)  let lost ~ binomial(customers, churn)  customers += new - lost  report customers * price by month as "MRR ($)"  market = next_month(market)  let growth ~ match market {  # month-on-month growth in sign-ups    Boom   => 8% to 20%    Steady => 1% to 6%    Slump  => normal_range(-8%, 0%)   # growth can be negative: say which shape you mean  }  signups *= 1 + growth}report customers * price >= 100_000 as "MRR ≥ $100k after 18 months"
test.problOpen in the playground
# A condition affects 1% of the population.let sick ~ bernoulli(1%)# Sensitivity is 95%; the false-positive rate is 8%.observe true from bernoulli(if sick { 95% } else { 8% })report sick
enumerated · evidence 8.87%sick    10.71%

Evidence

Update a model with evidence

observe conditions the model on evidence. A boolean observation removes incompatible worlds; an observation from a distribution weights worlds by the likelihood of the observed value.

In this example, prevalence is 1%, sensitivity is 95%, and the false-positive rate is 8%. Given a positive test, the posterior probability is 10.71%. The run also reports the probability of observing a positive result.

Models can read typed CSV, JSON and text inputs. Supported conjugate pairs, such as a beta prior with binomial observations, update analytically in enumeration and sampling.

Continuous models

Analytic calculations where supported

Enumeration can keep a continuous draw as a distribution through affine calculations, thresholds, abs, min, max and clamp. Supported conjugate observations update its posterior without sampling individual values.

continuous.problOpen in the playground
let arrival ~ uniform(0, 30)observe arrival > 10let wait = max(arrival - 15, 0)report wait as "wait (minutes)"report wait > 10 as "waiting more than 10 minutes"
enumerated · evidence 66.67%wait (minutes)                  mean 5.62 · sd 4.96 · 5% 0.00 · median 5.00 · 95% 14.00waiting more than 10 minutes    25.00%

Condition an arrival time on being later than 10 minutes, then calculate the remaining wait. The result includes a 25% chance of no wait.

posterior.problOpen in the playground
let rate ~ beta(2, 2)observe 3 from binomial(5, rate)report ratereport rate > 50%
enumerated · evidence 21.43%rate          mean 0.56 · sd 0.16 · 5% 0.29 · median 0.56 · 95% 0.81rate > 50%    63.67%

Three successes in five trials update a beta(2, 2) prior to beta(5, 4). Later uses of the same draw see that posterior.

Interpreting results

Results, methods and limits

Execution mode and diagnostics

Reports identify enumeration or sampling and show evidence and unresolved weight where applicable. Enumeration uses floating-point weights; fractions displayed with ≈ are approximations.

enumerated · evidence 8.87%
sample · 50,000 runs · seed 11

Reproducible runs

Pin the engine version, program, inputs, date and random seed to reproduce a successful run. Sampled results do not depend on the number of worker threads. Native and WebAssembly implementations are checked for matching results.

Independent checks

An independent interpreter using rational arithmetic checks the finite discrete subset on generated programs. Known-answer tests and sampling comparisons cover additional behavior. See the testing guide for coverage and remaining gaps.

A recipe and a shared outcome are different

= can bind a distribution recipe. Combining that recipe with itself describes independent draws. ~ binds one outcome per world; using that value twice refers to the same event. Here the probabilities are 9% and 30%.

storm.problOpen in the playground
let storm = bernoulli(30%)report storm and storm as "two independent storms"let happened ~ stormreport happened and happened as "the same storm twice"
enumeratedtwo independent storms    9.00%the same storm twice      30.00%

Get started

Run Probl

The playground

The browser editor provides completion, hover help, definitions and renaming. It includes a guide, reference and runnable examples. Shared links contain the program.

Open the playground

The command line

Install the CLI from crates.io with Rust 1.85 or later. The package is probl-cli; the command is probl:

$ cargo install probl-cli --locked
$ probl run model.probl
$ probl run model.probl --runs 100000 --seed 3
$ probl check model.probl
$ probl repl

Examples

19 example models

Examples cover games, forecasting, calendars, decisions, monitoring and risk. Each includes assumptions and expected results, and opens in the playground.

Status and scope

Probl 0.2.1 is early-stage software. The language and Rust API are evolving, and some type errors are detected at runtime. General nonlinear continuous inference, modules and quantum-amplitude simulation are not implemented. Review the reference semantics for current boundaries.

Probl draws on ideas from Squiggle, AnyDice and WebPPL. Source code is available on GitHub under MIT or Apache-2.0, without warranty.