FOPPL Evaluation and Likelihood Weighting

a first-order language · evaluation as σ-threading · sample & observe in the evaluator · likelihood weighting = importance sampling

Javier Burroni

2026-06-24

Today

Learning Objectives

  1. Define the FOPPL: ordinary expressions + sample + observe + statically bounded loops, so the set of random variables is finite and known before running.
  2. Read an evaluator’s judgment \(\rho, \ell, \sigma, e \Downarrow v, \sigma'\): in global environment \(\rho\) and local environment \(\ell\), evaluation threads an inference state \(\sigma\); one execution yields a return value, the random choices made, and a log weight.
  3. State the two probabilistic rules: sample draws from the prior and moves on; observe draws nothing and adds \(\log p_d(y)\) to \(\sigma\).
  4. Recognize likelihood weighting = importance sampling with the prior as proposal, estimated by the self-normalized average \(\sum w f / \sum w\).

Where we are

What we did

We saw why exact rejection fails for continuous observations: matching a value such as \(y = 2.3\) has probability zero. The escape is to score executions instead of filtering them.

  • Stage 1 of the course: the PPL is an interpreted s-expression language, and inference is a recursive eval that threads a state \(\sigma\).
  • Today: (1) define the language the evaluator reads, and (2) turn “run forward many times, score with observe” into a real, general algorithm: likelihood weighting.
  • The question we keep asking: given \(y = 2.3\), what is \(\mu\)? Now we answer it for a first-order program.

Section 1: The FOPPL Language

FOPPL and HOPPL

The book introduces two core language families.

language random-variable cardinality typical restrictions
FOPPL statically bounded no recursion; bounded loops; first-order functions
HOPPL not statically bounded recursion; higher-order functions; dynamic control flow

The important distinction is not just first-order vs higher-order. It is whether the number of random variables is statically bounded.

Why this distinction matters

In designing these languages our aim, however, was to establish the difference between finite random variable cardinality languages (FRVCL) and unbounded random variable cardinality languages (URVCL). These acronyms are terrible.

statically bounded not statically bounded
BUGS, JAGS, Stan Church, Anglican, WebPPL, Venture
model structure fixed before execution model structure may depend on execution
FOPPL is the toy language for this case HOPPL is the toy language for this case

Today we start with the statically bounded case: FOPPL.

Question 1

Four candidate programs. Three lay out a fixed set of random variables before running; one does not.

;; 🥗
(let [x (sample (normal 0 1))]
  (observe (normal x 1) 2.3)
  x)

;; 🍕
(let [b (sample (bernoulli 0.5))]
  (if (= b 1) (sample (normal 0 1)) 0))
;; 🎂
(foreach 3 [m [0 1 2]]
  (sample (normal m 1)))

;; 🍦
(defn geo [p]
  (if (= 1 (sample (bernoulli p)))
      1 (+ 1 (geo p))))
(geo 0.5)

Which one is NOT a legal FOPPL program?

A) Program 🍦: it recurses until a random event, so the number of sample calls is not bounded before running.

B) Program 🥗: observe may not appear as a statement whose value is discarded.

C) Program 🍕: if is not part of the first-order language.

D) All four are legal: every one parses as a FOPPL expression.

What the FOPPL is

A Lisp variant (Clojure-flavored). Eight expression forms, six ordinary and two probabilistic:

e ::= c | v | (let [v e1] e2) | (if e1 e2 e3)
    | (f e1 ... en) | (c e1 ... en)
    | (sample e) | (observe e1 e2)
  • Six ordinary forms: constants, variables, let, if, user/primitive calls. This is an ordinary deterministic language.
  • (sample d): draw a value from distribution d. (observe d v): declare v was drawn from d.
  • Two restrictions: no recursion, and no higher-order functions.

The defining property

With those restrictions, every program describes a finite set of random variables, and that set can be determined before the program runs.

Why loops must be bounded

foreach and loop take a constant iteration count, not an expression:

Legal: count is a constant

(foreach 5 [x data]
  (observe (normal x 1) x))

Illegal: count is sampled

(let [m (sample (poisson 10))]
  (foreach m []
    (sample (normal 0 1))))
  • A constant count means each loop is a finite expansion: it unrolls to a fixed list of let forms at compile time.
  • A data-dependent count (or recursion) would make the number of random variables depend on the run. FOPPL forbids exactly this.

Sugar, set aside

let with many bindings, foreach, loop, vector and map literals are readability sugar that desugars to the eight core forms. We build the core evaluator, not the surface syntax.

Question 2

A legal FOPPL program: one latent mean, three observations in a bounded loop.

(let [mu (sample (normal 0 1))]
  (foreach 3 [y [2.1 0.4 1.7]]
    (observe (normal mu 1) y))
  mu)

How many latent random choices are sampled during one execution?

A) Four: the latent mu, plus one random variable for each of the three observe forms.

B) One: only mu is a random variable; the three observe forms condition on fixed data, they add no latent variables.

C) Three: the foreach runs three times, so it creates three random variables.

D) Unknown until the program runs and the loop actually executes.

Deterministic evaluation, environments

Strip away sample and observe and the FOPPL is an ordinary expression evaluator over an environment \(\rho\):

  • let extends \(\rho\): evaluate e1 to a value, bind it, evaluate the body in the extended \(\rho\).
  • A variable looks up its value in \(\rho\). if evaluates one branch (lazy). Calls bind arguments in a fresh \(\rho\).
  • Binding is lexical: a reference means the binding visible where it is written, fixed when that let is evaluated.
(let [x (+ 1 2)            ; x bound to 3
      y (* x x)]           ; y sees x = 3, so 9
  (+ x y))                 ; => 12

The deterministic core comes for free

Everything except sample and observe is just a small interpreter. The probabilistic content lives in two forms and in the state those forms touch.

Section 2: Executing a Program: Traces and Scores

Three layers of notation

We are about to describe what one run does. Keep three things separate:

layer what it is example
object language the FOPPL program being run (observe (normal x 1) 2.3)
semantic notation how we describe the evaluator \(\rho, \ell, \sigma, e \Downarrow v, \sigma'\)
implementation how we code it a Python dict { "logW": ... }

The judgment reads: in global environment \(\rho\), local environment \(\ell\), and starting inference state \(\sigma\), expression \(e\) evaluates to value \(v\) and produces an updated inference state \(\sigma'\).

σ is the inference state

\(\rho\) stores global definitions; \(\ell\) stores local variable bindings. \(\sigma\) is the inference state, threaded through every evaluation. The notation is not part of the language; it is how we study the evaluator. For likelihood weighting, \(\sigma\) holds one number: \(\log W\).

One execution = value + choices + score

Run forward, threading \(\sigma\), with \(\sigma\) initialized to \(\log W = 0\):

  • At (sample d): draw \(x \sim d\) (from the prior) and continue. The draw is a random choice the run made.
  • At (observe d v): draw nothing. Add \(\log p_d(v)\) to \(\sigma\)’s \(\log W\), return \(v\), continue.
  • At the end: return value \(r\), the choices made, and \(\log W = \sum_{\text{observes}} \log p_d(v) = \log p(Y \given X)\).
# one forward run of Q1's 🥗 program, by hand
x = random.gauss(0, 1)                 # (sample (normal 0 1))
logW = norm_logpdf(2.3, loc=x, sd=1)   # (observe (normal x 1) 2.3)
return x, logW

sample moves, observe scores

sample draws. observe scores. Both return a value and evaluation continues.

Explaining Fig 4.1

Question 3

One run by hand worked for one program. Here is the general evaluator: a single recursive function that threads the inference state state (it carries the rng and the running log_w). Four forms are special; everything else is a primitive call.

def evaluate(expr, env, state):
    if isinstance(expr, Symbol):              # variable
        return env[expr]
    if not isinstance(expr, list):            # constant
        return expr
    op, *args = expr
    if op == "let":                           # (let [v e ...] body)
      ...

In your notebook, complete the if case so the evaluator runs. Hint: if is lazy and threads state like every other form, evaluate the test, then evaluate only the branch it selects.

Summary

The FOPPL

Ordinary expressions + sample + observe + bounded loops. No recursion, no higher-order functions, so the set of random variables is finite and known before running. Loops are finite expansions.

Evaluation threads σ

One execution gives a return value, the random choices made, and a log weight in the inference state \(\sigma\), described by \(\rho, \ell, \sigma, e \Downarrow v, \sigma'\). sample draws from the prior and moves on; observe draws nothing and adds \(\log p_d(y)\).

Next class

Reading

Before next class, read:

  • Chapter 4.2: Metropolis-Hastings
    • 4.2.1: single-site proposals
    • the addressing transformation
    • evaluating proposals
  • Chapter 3.2: Evaluating the density (lightly: the trace score is the unnormalized target)

Focus while reading

Pay attention to:

  • why the execution trace can be the inference state, not just a number;
  • how a random choice keeps the same identity across two runs;
  • what a single-site proposal changes, and what it can reuse;
  • which terms cancel in the acceptance ratio.

Questions: Answer Key

Solutions

  • Q1: A (Program 🍦 is not legal). Recursion makes the number of sample calls run-dependent, which FOPPL forbids; observe-as-statement (🥗) and if (🍕) are both legal core forms.
  • Q2: B (one). Only the sample site mu is a latent random variable; the three observes condition on fixed data and add none, and the count is fixed before running because foreach has a constant bound. A counts observes as variables, C counts loop iterations as new variables, D forgets that the structure is static.