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