Skip to main content

fugue/runtime/
handler.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/runtime/handler.md"))]
2
3use crate::core::address::Address;
4use crate::core::distribution::Distribution;
5use crate::core::model::Model;
6use crate::runtime::trace::Trace;
7
8/// Core trait for interpreting probabilistic model effects.
9///
10/// Handlers define how to interpret the three fundamental effects in probabilistic programming:
11/// sampling, observation, and factoring. Different implementations enable different execution modes.
12///
13/// To change what an existing handler does at some kinds of site and forward the rest to it,
14/// wrap it in a [`Delegate`](crate::runtime::delegate::Delegate) instead of implementing every
15/// method.
16///
17/// Example:
18/// ```rust
19/// # use fugue::*;
20/// # use fugue::runtime::interpreters::PriorHandler;
21/// # use rand::rngs::StdRng;
22/// # use rand::SeedableRng;
23///
24/// // Use a built-in handler
25/// let mut rng = StdRng::seed_from_u64(42);
26/// let handler = PriorHandler {
27///     rng: &mut rng,
28///     trace: Trace::default()
29/// };
30/// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
31/// let (result, trace) = runtime::handler::run(handler, model);
32/// ```
33pub trait Handler {
34    /// Handle an f64 sampling operation (continuous distributions).
35    fn on_sample_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>) -> f64;
36
37    /// Handle a bool sampling operation (Bernoulli).
38    fn on_sample_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>) -> bool;
39
40    /// Handle a u64 sampling operation (Poisson, Binomial).
41    fn on_sample_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>) -> u64;
42
43    /// Handle a usize sampling operation (Categorical).
44    fn on_sample_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>) -> usize;
45
46    /// Handle an i64 sampling operation (signed discrete distributions).
47    ///
48    /// This has a default implementation that panics so that handlers written
49    /// before the i64 sample path existed keep compiling unchanged; every
50    /// handler shipped in this crate overrides it. A model only reaches this
51    /// method if it contains a [`Model::SampleI64`](crate::Model::SampleI64)
52    /// node (e.g. [`DiscreteUniform`](crate::DiscreteUniform)).
53    fn on_sample_i64(&mut self, addr: &Address, _dist: &dyn Distribution<i64>) -> i64 {
54        panic!(
55            "handler does not implement on_sample_i64 (i64 sample site at {})",
56            addr
57        )
58    }
59
60    /// Handle an f64 observation operation.
61    fn on_observe_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>, value: f64);
62
63    /// Handle a bool observation operation.
64    fn on_observe_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>, value: bool);
65
66    /// Handle a u64 observation operation.
67    fn on_observe_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>, value: u64);
68
69    /// Handle a usize observation operation.
70    fn on_observe_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>, value: usize);
71
72    /// Handle an i64 observation operation.
73    ///
74    /// Defaults to a panic for the same forward-compatibility reason as
75    /// [`Handler::on_sample_i64`]; all in-crate handlers override it.
76    fn on_observe_i64(&mut self, addr: &Address, _dist: &dyn Distribution<i64>, _value: i64) {
77        panic!(
78            "handler does not implement on_observe_i64 (i64 observe site at {})",
79            addr
80        )
81    }
82
83    /// Handle a factor operation.
84    ///
85    /// This method is called when the model encounters a `factor` operation.
86    /// The handler typically adds the log-weight to the trace.
87    ///
88    /// # Arguments
89    ///
90    /// * `logw` - Log-weight to add to the model's total weight
91    fn on_factor(&mut self, logw: f64);
92
93    /// Finalize the handler and return the accumulated trace.
94    ///
95    /// This method is called after model execution completes to retrieve
96    /// the final trace containing all choices and log-weights.
97    fn finish(self) -> Trace
98    where
99        Self: Sized;
100}
101
102/// Execute a probabilistic model using the provided handler.
103///
104/// This is the core execution engine for probabilistic models. It walks through
105/// the model structure and dispatches effects to the handler, returning both
106/// the model's final result and the accumulated execution trace.
107///
108/// Example:
109/// ```rust
110/// # use fugue::*;
111/// # use fugue::runtime::interpreters::PriorHandler;
112/// # use rand::rngs::StdRng;
113/// # use rand::SeedableRng;
114///
115/// // Create a simple model
116/// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
117///     .bind(|x| observe(addr!("y"), Normal::new(x, 0.1).unwrap(), 1.2))
118///     .map(|_| "completed");
119///
120/// let mut rng = StdRng::seed_from_u64(123);
121/// let (result, trace) = runtime::handler::run(
122///     PriorHandler { rng: &mut rng, trace: Trace::default() },
123///     model
124/// );
125/// assert_eq!(result, "completed");
126/// assert!(trace.total_log_weight().is_finite());
127/// ```
128pub fn run<A>(mut h: impl Handler, m: Model<A>) -> (A, Trace) {
129    // Iterative trampoline (FG-19): the model is a CPS-encoded linked list of
130    // continuations, so we interpret it in an explicit loop instead of the old
131    // `go(h, k(x))` recursion. This keeps interpretation O(1) in stack depth
132    // regardless of model length, so deep chains (e.g. `plate!`/`sequence_vec`
133    // over thousands of sites, or a 100k-deep sample+bind loop) no longer
134    // overflow the stack. Each effectful node advances `m` to its continuation
135    // `k(value)` and loops; only `Model::Pure` terminates.
136    let mut m = m;
137    let a = loop {
138        m = match m {
139            Model::Pure(a) => break a,
140            Model::SampleF64 { addr, dist, k } => {
141                let x = h.on_sample_f64(&addr, &*dist);
142                k(x)
143            }
144            Model::SampleBool { addr, dist, k } => {
145                let x = h.on_sample_bool(&addr, &*dist);
146                k(x)
147            }
148            Model::SampleU64 { addr, dist, k } => {
149                let x = h.on_sample_u64(&addr, &*dist);
150                k(x)
151            }
152            Model::SampleUsize { addr, dist, k } => {
153                let x = h.on_sample_usize(&addr, &*dist);
154                k(x)
155            }
156            Model::SampleI64 { addr, dist, k } => {
157                let x = h.on_sample_i64(&addr, &*dist);
158                k(x)
159            }
160            Model::ObserveF64 {
161                addr,
162                dist,
163                value,
164                k,
165            } => {
166                h.on_observe_f64(&addr, &*dist, value);
167                k(())
168            }
169            Model::ObserveBool {
170                addr,
171                dist,
172                value,
173                k,
174            } => {
175                h.on_observe_bool(&addr, &*dist, value);
176                k(())
177            }
178            Model::ObserveU64 {
179                addr,
180                dist,
181                value,
182                k,
183            } => {
184                h.on_observe_u64(&addr, &*dist, value);
185                k(())
186            }
187            Model::ObserveUsize {
188                addr,
189                dist,
190                value,
191                k,
192            } => {
193                h.on_observe_usize(&addr, &*dist, value);
194                k(())
195            }
196            Model::ObserveI64 {
197                addr,
198                dist,
199                value,
200                k,
201            } => {
202                h.on_observe_i64(&addr, &*dist, value);
203                k(())
204            }
205            Model::Factor { logw, k } => {
206                h.on_factor(logw);
207                k(())
208            }
209        };
210    };
211    let t = h.finish();
212    (a, t)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::addr;
219    use crate::core::distribution::*;
220    use crate::core::model::ModelExt;
221    use crate::runtime::interpreters::PriorHandler;
222    use rand::rngs::StdRng;
223    use rand::SeedableRng;
224
225    #[test]
226    fn run_accumulates_logs_for_sample_observe_factor() {
227        // Model: sample x ~ Normal(0,1); observe y ~ Normal(x,1) with value 0.5; factor(-1.0)
228        let model = crate::core::model::sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
229            .and_then(|x| {
230                crate::core::model::observe(addr!("y"), Normal::new(x, 1.0).unwrap(), 0.5)
231            })
232            .and_then(|_| crate::core::model::factor(-1.0));
233
234        let mut rng = StdRng::seed_from_u64(123);
235        let (_a, trace) = crate::runtime::handler::run(
236            PriorHandler {
237                rng: &mut rng,
238                trace: Trace::default(),
239            },
240            model,
241        );
242
243        // Should have a sample recorded and finite prior
244        assert!(trace.choices.contains_key(&addr!("x")));
245        assert!(trace.log_prior.is_finite());
246        // Observation contributes to likelihood
247        assert!(trace.log_likelihood.is_finite());
248        // Factor contributes exact -1.0
249        assert!((trace.log_factors + 1.0).abs() < 1e-12);
250    }
251
252    // Regression for FG-19: interpretation must be stack-safe. Before the
253    // trampoline, `run` recursed once per effectful node (`go(h, k(x))`), so a
254    // deep sample+bind chain overflowed the stack. This model is a loop of
255    // 100_000 sequential `sample`+`bind` sites (the accumulator is threaded as a
256    // plain parameter so each continuation directly yields the next node); it
257    // overflows the stack on the pre-fix recursive interpreter and completes in
258    // O(1) stack on the trampoline. Runs on a small-stack thread to make the
259    // guarantee explicit rather than relying on the test harness's stack size.
260    #[test]
261    fn interpretation_is_stack_safe_for_deep_models() {
262        fn build(i: usize, n: usize, acc: f64) -> Model<f64> {
263            if i >= n {
264                crate::core::model::pure(acc)
265            } else {
266                crate::core::model::sample(addr!("x", i), Normal::new(0.0, 1.0).unwrap())
267                    .bind(move |x| build(i + 1, n, acc + x))
268            }
269        }
270
271        // 512 KiB stack: comfortably too small for 100_000 recursive frames,
272        // but ample for the constant-stack trampoline.
273        let handle = std::thread::Builder::new()
274            .stack_size(512 * 1024)
275            .spawn(|| {
276                let n = 100_000;
277                let mut rng = StdRng::seed_from_u64(2024);
278                let (sum, trace) = crate::runtime::handler::run(
279                    PriorHandler {
280                        rng: &mut rng,
281                        trace: Trace::default(),
282                    },
283                    build(0, n, 0.0),
284                );
285                assert!(sum.is_finite());
286                assert_eq!(trace.choices.len(), n);
287                assert!(trace.log_prior.is_finite());
288            })
289            .expect("spawn thread");
290        handle
291            .join()
292            .expect("deep model interpretation overflowed the stack");
293    }
294
295    // Companion regression from PR #34: the same stack-safety guarantee through
296    // the `sequence_vec` + `observe` path (the deep test above covers
297    // sample+bind). 100k observations interpreted through `sequence_vec` must
298    // complete without overflowing the stack.
299    #[test]
300    fn run_handles_large_observe_sequence() {
301        use crate::core::model::{observe, sequence_vec};
302
303        let n = 100_000usize;
304        let models: Vec<Model<()>> = (0..n)
305            .map(|i| observe(addr!("obs", i), Bernoulli::new(0.5).unwrap(), true))
306            .collect();
307        let model = sequence_vec(models).map(|_| ());
308
309        let mut rng = StdRng::seed_from_u64(123);
310        let (_a, trace) = crate::runtime::handler::run(
311            PriorHandler {
312                rng: &mut rng,
313                trace: Trace::default(),
314            },
315            model,
316        );
317
318        assert!(trace.log_likelihood.is_finite());
319    }
320}