Skip to main content

fugue/runtime/
interpreters.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/runtime/interpreters.md"))]
2
3use crate::core::address::Address;
4use crate::core::distribution::Distribution;
5use crate::core::model::Model;
6use crate::error::{ErrorCode, FugueError, FugueResult};
7use crate::runtime::handler::{run, Handler};
8use crate::runtime::trace::{Choice, ChoiceValue, Trace};
9
10use rand::RngCore;
11
12/// Panic when a sample site reuses an address already recorded in this
13/// execution's output trace (FG-47).
14///
15/// "Fast" handlers (`PriorHandler`, `ReplayHandler`, `ScoreGivenTrace`) treat a
16/// duplicate address as a programming error and panic with a precise message —
17/// this is the documented fast/safe split. Detection is O(log n) and needs no
18/// extra state: the output trace's `choices` map already contains exactly the
19/// addresses visited so far in this run, so a hit means the address was sampled
20/// twice. Before this check, the second visit silently double-counted its
21/// log-prior while dropping the first choice.
22#[inline]
23fn assert_no_duplicate_sample(trace: &Trace, addr: &Address, handler: &str) {
24    if trace.choices.contains_key(addr) {
25        panic!(
26            "{handler}: address {addr} was sampled twice in one execution \
27             (AddressConflict, ErrorCode::{:?}={}). Every sample site must have \
28             a unique address.",
29            ErrorCode::AddressConflict,
30            ErrorCode::AddressConflict as u32
31        );
32    }
33}
34
35/// Build the [`FugueError`] used for a duplicate sample address, so that "safe"
36/// handlers and the fallible scoring paths surface a real
37/// [`ErrorCode::AddressConflict`] (FG-47) instead of panicking.
38fn address_conflict_error(addr: &Address, handler: &str) -> FugueError {
39    FugueError::ModelError {
40        address: Some(addr.clone()),
41        reason: format!("{handler}: address sampled twice in one execution"),
42        code: ErrorCode::AddressConflict,
43        context: crate::error::ErrorContext::new(),
44    }
45}
46
47// =============================================================================
48// Internal monomorphization macros (FG-54)
49//
50// Each handler used to hand-copy four (now five, with i64) near-identical
51// `on_sample_*` / `on_observe_*` methods. These `macro_rules!` collapse that
52// copy-paste: a handler lists the `(method, type, variant)` rows once and the
53// macro expands the shared body for every supported value type. The behavior is
54// identical to the old hand-written methods, plus the FG-47 duplicate-address
55// check and the FG-48 fresh-logp fix, applied uniformly.
56// =============================================================================
57
58/// The five value types every handler supports, as `(sample_method,
59/// observe_method, rust_type, ChoiceValue variant, type-name literal,
60/// Option-getter, Result-getter)` rows. Passed to the per-handler macros so the
61/// row list lives in exactly one place.
62macro_rules! for_each_value_type {
63    ($m:ident) => {
64        $m! {
65            (on_sample_f64,   on_observe_f64,   f64,   F64,   "f64",   get_f64,   get_f64_result),
66            (on_sample_bool,  on_observe_bool,  bool,  Bool,  "bool",  get_bool,  get_bool_result),
67            (on_sample_u64,   on_observe_u64,   u64,   U64,   "u64",   get_u64,   get_u64_result),
68            (on_sample_usize, on_observe_usize, usize, Usize, "usize", get_usize, get_usize_result),
69            (on_sample_i64,   on_observe_i64,   i64,   I64,   "i64",   get_i64,   get_i64_result),
70        }
71    };
72}
73
74/// Generate the five identical `on_observe_*` methods (every handler scores an
75/// observation the same way: add its log-density to `log_likelihood`).
76macro_rules! impl_observe_methods {
77    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
78        $(
79            fn $observe(&mut self, _addr: &Address, dist: &dyn Distribution<$ty>, value: $ty) {
80                self.trace.log_likelihood += dist.log_prob(&value);
81            }
82        )*
83    };
84}
85
86/// `PriorHandler`: draw a fresh value, score it, record it. Panics on a
87/// duplicate address (fast handler).
88macro_rules! impl_prior_sample_methods {
89    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
90        $(
91            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
92                assert_no_duplicate_sample(&self.trace, addr, "PriorHandler");
93                let x = dist.sample(self.rng);
94                let lp = dist.log_prob(&x);
95                self.trace.log_prior += lp;
96                self.trace.choices.insert(
97                    addr.clone(),
98                    Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
99                );
100                x
101            }
102        )*
103    };
104}
105
106/// `ReplayHandler`: reuse the base value if present (panic on type mismatch),
107/// otherwise sample fresh; always re-score under the current distribution.
108/// Panics on a duplicate address (fast handler).
109macro_rules! impl_replay_sample_methods {
110    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
111        $(
112            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
113                assert_no_duplicate_sample(&self.trace, addr, "ReplayHandler");
114                let x = if let Some(c) = self.base.choices.get(addr) {
115                    match c.value {
116                        ChoiceValue::$variant(v) => v,
117                        _ => panic!("expected {} at {}", $tyname, addr),
118                    }
119                } else {
120                    dist.sample(self.rng)
121                };
122                let lp = dist.log_prob(&x);
123                self.trace.log_prior += lp;
124                self.trace.choices.insert(
125                    addr.clone(),
126                    Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
127                );
128                x
129            }
130        )*
131    };
132}
133
134/// `ScoreGivenTrace`: read the fixed value from the base trace (panic if
135/// missing or wrong type), score it under the current distribution, and store a
136/// FRESH choice carrying that newly computed logp (FG-48). Panics on a duplicate
137/// address (fast handler).
138macro_rules! impl_score_sample_methods {
139    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
140        $(
141            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
142                assert_no_duplicate_sample(&self.trace, addr, "ScoreGivenTrace");
143                let c = self
144                    .base
145                    .choices
146                    .get(addr)
147                    .unwrap_or_else(|| panic!("missing value for site {} in base trace", addr));
148                let x = match c.value {
149                    ChoiceValue::$variant(v) => v,
150                    _ => panic!("expected {} at {}", $tyname, addr),
151                };
152                let lp = dist.log_prob(&x);
153                self.trace.log_prior += lp;
154                // FG-48: store the freshly computed logp, not the stale base one.
155                self.trace.choices.insert(
156                    addr.clone(),
157                    Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
158                );
159                x
160            }
161        )*
162    };
163}
164
165/// `SafeReplayHandler`: like `ReplayHandler` but recovers from missing/mismatched
166/// base values by sampling fresh (optionally warning). A duplicate address is a
167/// programming error even in the safe handler, so it invalidates the trace with
168/// `-inf` and warns rather than silently double-counting (FG-47).
169macro_rules! impl_safe_replay_sample_methods {
170    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
171        $(
172            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
173                if self.trace.choices.contains_key(addr) {
174                    if self.warn_on_mismatch {
175                        eprintln!("Warning: {}", address_conflict_error(addr, "SafeReplayHandler"));
176                    }
177                    self.trace.log_prior += f64::NEG_INFINITY;
178                    return dist.sample(self.rng);
179                }
180                let x = match self.base.$get(addr) {
181                    Some(v) => v,
182                    None => {
183                        if self.warn_on_mismatch && self.base.choices.contains_key(addr) {
184                            if let Some(choice) = self.base.choices.get(addr) {
185                                eprintln!(
186                                    "Warning: Type mismatch at {}: expected {}, found {}",
187                                    addr, $tyname, choice.value.type_name()
188                                );
189                            }
190                        }
191                        dist.sample(self.rng)
192                    }
193                };
194                let lp = dist.log_prob(&x);
195                self.trace.log_prior += lp;
196                self.trace.choices.insert(
197                    addr.clone(),
198                    Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
199                );
200                x
201            }
202        )*
203    };
204}
205
206/// `SafeScoreGivenTrace`: like `ScoreGivenTrace` but returns an invalid (`-inf`)
207/// trace instead of panicking on a missing/mismatched address, and stores a
208/// FRESH choice with the newly computed logp (FG-48). A duplicate address
209/// invalidates the trace (FG-47).
210macro_rules! impl_safe_score_sample_methods {
211    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
212        $(
213            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
214                if self.trace.choices.contains_key(addr) {
215                    if self.warn_on_error {
216                        eprintln!("Warning: {}", address_conflict_error(addr, "SafeScoreGivenTrace"));
217                    }
218                    self.trace.log_prior += f64::NEG_INFINITY;
219                    return <$ty as Default>::default();
220                }
221                match self.base.$get_res(addr) {
222                    Ok(x) => {
223                        let lp = dist.log_prob(&x);
224                        self.trace.log_prior += lp;
225                        // FG-48: fresh logp consistent with what we accumulated.
226                        self.trace.choices.insert(
227                            addr.clone(),
228                            Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
229                        );
230                        x
231                    }
232                    Err(e) => {
233                        if self.warn_on_error {
234                            eprintln!("Warning: Failed to get {} at {}: {}", $tyname, addr, e);
235                        }
236                        self.trace.log_prior += f64::NEG_INFINITY;
237                        <$ty as Default>::default()
238                    }
239                }
240            }
241        )*
242    };
243}
244
245/// `StrictScoreGivenTrace`: the fallible, structure-checking scoring path
246/// (FG-20/FG-21). It records the FIRST structural problem into `self.error`
247/// instead of panicking: an address absent from the base trace or a type
248/// mismatch yields `ErrorCode::UnexpectedModelStructure`; a duplicate address
249/// yields `ErrorCode::AddressConflict`. On success it stores a fresh, correctly
250/// scored choice.
251macro_rules! impl_strict_score_sample_methods {
252    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
253        $(
254            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
255                if self.trace.choices.contains_key(addr) {
256                    if self.error.is_none() {
257                        *self.error = Some(address_conflict_error(addr, "StrictScoreGivenTrace"));
258                    }
259                    return <$ty as Default>::default();
260                }
261                match self.base.$get_res(addr) {
262                    Ok(x) => {
263                        let lp = dist.log_prob(&x);
264                        self.trace.log_prior += lp;
265                        self.trace.choices.insert(
266                            addr.clone(),
267                            Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
268                        );
269                        x
270                    }
271                    Err(cause) => {
272                        if self.error.is_none() {
273                            *self.error = Some(FugueError::ModelError {
274                                address: Some(addr.clone()),
275                                reason: format!(
276                                    "model visited address {} not present (as {}) in the base \
277                                     trace; structure varies between the base trace and this model",
278                                    addr, $tyname
279                                ),
280                                code: ErrorCode::UnexpectedModelStructure,
281                                context: crate::error::ErrorContext::new().with_cause(cause),
282                            });
283                        }
284                        <$ty as Default>::default()
285                    }
286                }
287            }
288        )*
289    };
290}
291
292/// `ReconcilingScoreGivenTrace`: the reconciling scoring path (FG-20/FG-21).
293/// Addresses present in the base trace are replayed and re-scored; NEW addresses
294/// (absent, or present with the wrong type) are sampled fresh from the prior and
295/// their log-prior accumulated (the RJMCMC-correct treatment of prior-proposed
296/// fresh dimensions) and recorded in `fresh`. Vanished addresses are computed
297/// after the run by the driver. A duplicate address is still an error.
298macro_rules! impl_reconciling_score_sample_methods {
299    ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
300        $(
301            fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
302                if self.trace.choices.contains_key(addr) {
303                    if self.error.is_none() {
304                        *self.error =
305                            Some(address_conflict_error(addr, "ReconcilingScoreGivenTrace"));
306                    }
307                    return <$ty as Default>::default();
308                }
309                let x = match self.base.$get(addr) {
310                    Some(v) => v,
311                    None => {
312                        // New (or type-changed) dimension: propose from the prior.
313                        self.fresh.push(addr.clone());
314                        dist.sample(self.rng)
315                    }
316                };
317                let lp = dist.log_prob(&x);
318                self.trace.log_prior += lp;
319                self.trace.choices.insert(
320                    addr.clone(),
321                    Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
322                );
323                x
324            }
325        )*
326    };
327}
328
329/// Handler for prior sampling - generates fresh random values from distributions.
330///
331/// This is the foundational interpreter that implements standard "forward sampling"
332/// from probabilistic models. It draws fresh values from distributions and accumulates
333/// log-probabilities in the trace.
334///
335/// Example:
336/// ```rust
337/// # use fugue::*;
338/// # use fugue::runtime::interpreters::PriorHandler;
339/// # use rand::rngs::StdRng;
340/// # use rand::SeedableRng;
341///
342/// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
343///     .bind(|x| observe(addr!("y"), Normal::new(x, 0.5).unwrap(), 1.2)
344///         .map(move |_| x));
345///
346/// let mut rng = StdRng::seed_from_u64(42);
347/// let (result, trace) = runtime::handler::run(
348///     PriorHandler { rng: &mut rng, trace: Trace::default() },
349///     model
350/// );
351///
352/// assert!(result.is_finite());
353/// assert!(trace.log_likelihood.is_finite());
354/// ```
355pub struct PriorHandler<'r, R: RngCore> {
356    /// Random number generator for sampling.
357    pub rng: &'r mut R,
358    /// Trace to accumulate execution history.
359    pub trace: Trace,
360}
361impl<'r, R: RngCore> Handler for PriorHandler<'r, R> {
362    for_each_value_type!(impl_prior_sample_methods);
363    for_each_value_type!(impl_observe_methods);
364
365    fn on_factor(&mut self, logw: f64) {
366        self.trace.log_factors += logw;
367    }
368
369    fn finish(self) -> Trace {
370        self.trace
371    }
372}
373
374/// Handler for replaying models with values from an existing trace.
375///
376/// ReplayHandler replays a model execution using stored trace values. When a sampling
377/// site is encountered: if the address exists in the base trace, use that value;
378/// if missing, sample fresh. Essential for MCMC where you replay most choices
379/// but sample new values at specific sites.
380///
381/// Example:
382/// ```rust
383/// # use fugue::*;
384/// # use fugue::runtime::interpreters::*;
385/// # use rand::rngs::StdRng;
386/// # use rand::SeedableRng;
387///
388/// // Create base trace
389/// let mut rng = StdRng::seed_from_u64(42);
390/// let model_fn = || sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
391/// let (original, base_trace) = runtime::handler::run(
392///     PriorHandler { rng: &mut rng, trace: Trace::default() },
393///     model_fn()
394/// );
395///
396/// // Replay using base trace values
397/// let (replayed, _) = runtime::handler::run(
398///     ReplayHandler {
399///         rng: &mut rng,
400///         base: base_trace,
401///         trace: Trace::default()
402///     },
403///     model_fn()
404/// );
405///
406/// assert_eq!(original, replayed); // Same value replayed
407/// ```
408pub struct ReplayHandler<'r, R: RngCore> {
409    /// Random number generator for sampling at addresses not in base trace.
410    pub rng: &'r mut R,
411    /// Base trace containing values to replay.
412    pub base: Trace,
413    /// New trace to accumulate the replay execution.
414    pub trace: Trace,
415}
416impl<'r, R: RngCore> Handler for ReplayHandler<'r, R> {
417    for_each_value_type!(impl_replay_sample_methods);
418    for_each_value_type!(impl_observe_methods);
419
420    fn on_factor(&mut self, logw: f64) {
421        self.trace.log_factors += logw;
422    }
423
424    fn finish(self) -> Trace {
425        self.trace
426    }
427}
428
429/// Handler for scoring a model given a complete trace of fixed choices.
430///
431/// ScoreGivenTrace computes log-probability of a model execution where all random
432/// choices are predetermined. No sampling occurs - values are looked up from the
433/// base trace and their log-probabilities computed. Essential for MCMC acceptance
434/// ratios, importance sampling, and model comparison.
435///
436/// Example:
437/// ```rust
438/// # use fugue::*;
439/// # use fugue::runtime::interpreters::*;
440/// # use rand::rngs::StdRng;
441/// # use rand::SeedableRng;
442///
443/// // Create a complete trace
444/// let mut rng = StdRng::seed_from_u64(42);
445/// let (_, complete_trace) = runtime::handler::run(
446///     PriorHandler { rng: &mut rng, trace: Trace::default() },
447///     sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
448/// );
449///
450/// // Score under different model parameters  
451/// let (value, score_trace) = runtime::handler::run(
452///     ScoreGivenTrace {
453///         base: complete_trace,
454///         trace: Trace::default()
455///     },
456///     sample(addr!("x"), Normal::new(1.0, 2.0).unwrap()) // Different parameters
457/// );
458///
459/// assert!(score_trace.total_log_weight().is_finite());
460/// ```
461pub struct ScoreGivenTrace {
462    /// Base trace containing the fixed choices to score.
463    pub base: Trace,
464    /// New trace to accumulate log-probabilities.
465    pub trace: Trace,
466}
467impl Handler for ScoreGivenTrace {
468    for_each_value_type!(impl_score_sample_methods);
469    for_each_value_type!(impl_observe_methods);
470
471    fn on_factor(&mut self, logw: f64) {
472        self.trace.log_factors += logw;
473    }
474
475    fn finish(self) -> Trace {
476        self.trace
477    }
478}
479
480/// Safe version of ReplayHandler that gracefully handles trace inconsistencies.
481///
482/// SafeReplayHandler replays model execution like ReplayHandler, but handles type
483/// mismatches and missing addresses gracefully by logging warnings and sampling
484/// fresh values instead of panicking. Essential for production systems where
485/// trace consistency cannot be guaranteed.
486///
487/// Example:
488/// ```rust
489/// # use fugue::*;
490/// # use fugue::runtime::interpreters::*;
491/// # use rand::rngs::StdRng;
492/// # use rand::SeedableRng;
493///
494/// // Create trace with potential inconsistencies
495/// let mut rng = StdRng::seed_from_u64(42);
496/// let (_, base_trace) = runtime::handler::run(
497///     PriorHandler { rng: &mut rng, trace: Trace::default() },
498///     sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()) // f64 value
499/// );
500///
501/// // Safe replay handles type mismatch gracefully
502/// let (result, trace) = runtime::handler::run(
503///     SafeReplayHandler {
504///         rng: &mut rng,
505///         base: base_trace,
506///         trace: Trace::default(),
507///         warn_on_mismatch: true, // Enable warnings
508///     },
509///     sample(addr!("x"), Bernoulli::new(0.5).unwrap()) // Expects bool
510/// );
511///
512/// assert!(trace.total_log_weight().is_finite()); // Continues execution
513/// ```
514pub struct SafeReplayHandler<'r, R: RngCore> {
515    /// Random number generator for sampling at addresses not in base trace.
516    pub rng: &'r mut R,
517    /// Base trace containing values to replay.
518    pub base: Trace,
519    /// New trace to accumulate the replay execution.
520    pub trace: Trace,
521    /// Whether to log warnings on type mismatches (useful for debugging).
522    pub warn_on_mismatch: bool,
523}
524impl<'r, R: RngCore> Handler for SafeReplayHandler<'r, R> {
525    for_each_value_type!(impl_safe_replay_sample_methods);
526    for_each_value_type!(impl_observe_methods);
527
528    fn on_factor(&mut self, logw: f64) {
529        self.trace.log_factors += logw;
530    }
531
532    fn finish(self) -> Trace {
533        self.trace
534    }
535}
536
537/// Safe version of ScoreGivenTrace that gracefully handles incomplete traces.
538///
539/// SafeScoreGivenTrace computes log-probability like ScoreGivenTrace, but handles
540/// missing addresses or type mismatches by returning negative infinity log-weight
541/// instead of panicking. Essential for production inference where trace validity
542/// cannot be guaranteed.
543///
544/// Example:
545/// ```rust
546/// # use fugue::*;
547/// # use fugue::runtime::interpreters::*;
548/// # use rand::rngs::StdRng;
549/// # use rand::SeedableRng;
550///
551/// // Create incomplete trace
552/// let mut rng = StdRng::seed_from_u64(42);
553/// let (_, incomplete_trace) = runtime::handler::run(
554///     PriorHandler { rng: &mut rng, trace: Trace::default() },
555///     sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()) // Only has "x"
556/// );
557///
558/// // Safe scoring handles missing address gracefully
559/// let (_, score_trace) = runtime::handler::run(
560///     SafeScoreGivenTrace {
561///         base: incomplete_trace,
562///         trace: Trace::default(),
563///         warn_on_error: true, // Enable warnings
564///     },
565///     sample(addr!("missing"), Normal::new(0.0, 1.0).unwrap()) // Address not in base
566/// );
567///
568/// assert_eq!(score_trace.total_log_weight(), f64::NEG_INFINITY); // Graceful failure
569/// ```
570pub struct SafeScoreGivenTrace {
571    /// Base trace containing the fixed choices to score.
572    pub base: Trace,
573    /// New trace to accumulate log-probabilities.
574    pub trace: Trace,
575    /// Whether to log warnings on missing addresses or type mismatches.
576    pub warn_on_error: bool,
577}
578impl Handler for SafeScoreGivenTrace {
579    for_each_value_type!(impl_safe_score_sample_methods);
580    for_each_value_type!(impl_observe_methods);
581
582    fn on_factor(&mut self, logw: f64) {
583        self.trace.log_factors += logw;
584    }
585
586    fn finish(self) -> Trace {
587        self.trace
588    }
589}
590
591// =============================================================================
592// Structure-varying (trans-dimensional) scoring paths (FG-20 / FG-21)
593// =============================================================================
594
595/// The strict, fallible sibling of [`ScoreGivenTrace`].
596///
597/// This handler backs [`score_given_trace_strict`]. Instead of panicking when
598/// the model's address structure differs from the base trace, it records the
599/// first structural problem so the driver can return a [`FugueError`]:
600///
601/// - visiting an address absent from the base trace (or present with the wrong
602///   value type) -> [`ErrorCode::UnexpectedModelStructure`];
603/// - visiting the same address twice -> [`ErrorCode::AddressConflict`].
604///
605/// On success every visited site stores a fresh, correctly scored choice.
606pub struct StrictScoreGivenTrace<'e> {
607    /// Base trace containing the fixed choices to score.
608    pub base: Trace,
609    /// New trace to accumulate log-probabilities.
610    pub trace: Trace,
611    /// First structural error encountered, surfaced by the driver.
612    error: &'e mut Option<FugueError>,
613}
614impl<'e> Handler for StrictScoreGivenTrace<'e> {
615    for_each_value_type!(impl_strict_score_sample_methods);
616    for_each_value_type!(impl_observe_methods);
617
618    fn on_factor(&mut self, logw: f64) {
619        self.trace.log_factors += logw;
620    }
621
622    fn finish(self) -> Trace {
623        self.trace
624    }
625}
626
627/// Score `model` against `base` strictly, returning an error rather than
628/// panicking when the model's address structure does not match the base trace
629/// (FG-20 / FG-21).
630///
631/// Returns `Ok((value, scored_trace))` when every sample site visited by the
632/// model is present in `base` with a matching value type. Returns `Err` with
633/// [`ErrorCode::UnexpectedModelStructure`] if the model visits an address absent
634/// from `base` (a branch opened by a differing latent value), or
635/// [`ErrorCode::AddressConflict`] if the model visits the same address twice.
636///
637/// This is the mechanism the MCMC layer needs to stop crashing on
638/// structure-varying proposals; wiring it into the samplers is a separate work
639/// package.
640///
641/// # Example
642///
643/// ```rust
644/// # use fugue::*;
645/// # use fugue::runtime::interpreters::{score_given_trace_strict, PriorHandler};
646/// # use rand::rngs::StdRng;
647/// # use rand::SeedableRng;
648/// let mut rng = StdRng::seed_from_u64(1);
649/// let (_, base) = runtime::handler::run(
650///     PriorHandler { rng: &mut rng, trace: Trace::default() },
651///     sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
652/// );
653///
654/// // Same structure: Ok.
655/// assert!(score_given_trace_strict(
656///     base.clone(),
657///     sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
658/// ).is_ok());
659///
660/// // Model reaches an address the base trace never recorded: Err.
661/// let err = score_given_trace_strict(
662///     base,
663///     sample(addr!("y"), Normal::new(0.0, 1.0).unwrap()),
664/// ).unwrap_err();
665/// assert_eq!(err.code(), ErrorCode::UnexpectedModelStructure);
666/// ```
667pub fn score_given_trace_strict<A>(base: Trace, model: Model<A>) -> FugueResult<(A, Trace)> {
668    let mut error: Option<FugueError> = None;
669    let handler = StrictScoreGivenTrace {
670        base,
671        trace: Trace::default(),
672        error: &mut error,
673    };
674    let (a, trace) = run(handler, model);
675    match error {
676        Some(e) => Err(e),
677        None => Ok((a, trace)),
678    }
679}
680
681/// Report of the structural differences reconciled by
682/// [`score_given_trace_reconciled`].
683#[derive(Clone, Debug, Default, PartialEq)]
684pub struct ReconcileReport {
685    /// Addresses visited by the model that were absent from the base trace (or
686    /// present with the wrong value type). Each was proposed fresh from its
687    /// prior and its `log_prob` accumulated into `log_prior`.
688    pub fresh_addresses: Vec<Address>,
689    /// Addresses present in the base trace that the model did NOT visit. Their
690    /// contribution should be dropped by the caller (e.g. removed from the
691    /// reverse-move density in an RJMCMC step).
692    pub vanished_addresses: Vec<Address>,
693}
694
695/// The reconciling sibling of [`ScoreGivenTrace`], backing
696/// [`score_given_trace_reconciled`].
697///
698/// Addresses present in the base trace are replayed and re-scored. NEW addresses
699/// (absent, or present with a different value type) are sampled fresh from the
700/// prior, their `log_prob` accumulated into `log_prior`, and recorded in
701/// `fresh`. A duplicate address is still an error.
702pub struct ReconcilingScoreGivenTrace<'r, 'f, 'e, R: RngCore> {
703    /// RNG used to propose fresh values for new addresses.
704    pub rng: &'r mut R,
705    /// Base trace containing the fixed choices to replay/score.
706    pub base: Trace,
707    /// New trace accumulating the reconciled execution.
708    pub trace: Trace,
709    /// Addresses sampled fresh from the prior (not present in `base`), in
710    /// visitation order. Borrowed so the driver can read it after `finish`.
711    fresh: &'f mut Vec<Address>,
712    /// First duplicate-address error encountered, surfaced by the driver.
713    error: &'e mut Option<FugueError>,
714}
715impl<'r, 'f, 'e, R: RngCore> Handler for ReconcilingScoreGivenTrace<'r, 'f, 'e, R> {
716    for_each_value_type!(impl_reconciling_score_sample_methods);
717    for_each_value_type!(impl_observe_methods);
718
719    fn on_factor(&mut self, logw: f64) {
720        self.trace.log_factors += logw;
721    }
722
723    fn finish(self) -> Trace {
724        self.trace
725    }
726}
727
728/// Score `model` against `base`, reconciling a differing address structure
729/// instead of panicking (FG-20 / FG-21).
730///
731/// Addresses shared with `base` are replayed and re-scored under the current
732/// model. Addresses the model introduces that are **not** in `base` (new
733/// branches) are sampled fresh from their prior and their log-prior accumulated
734/// — the RJMCMC-correct treatment of prior-proposed fresh dimensions. Addresses
735/// in `base` that the model does **not** visit are reported as
736/// [`ReconcileReport::vanished_addresses`] so the caller can drop their
737/// contribution.
738///
739/// Returns `Err` with [`ErrorCode::AddressConflict`] only if the model visits
740/// the same address twice.
741///
742/// # Example
743///
744/// ```rust
745/// # use fugue::*;
746/// # use fugue::runtime::interpreters::{score_given_trace_reconciled, PriorHandler};
747/// # use rand::rngs::StdRng;
748/// # use rand::SeedableRng;
749/// let mut rng = StdRng::seed_from_u64(7);
750/// let (_, base) = runtime::handler::run(
751///     PriorHandler { rng: &mut rng, trace: Trace::default() },
752///     sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
753/// );
754///
755/// // Model drops "x" and introduces "y": "y" is proposed fresh, "x" vanished.
756/// let (_v, trace, report) = score_given_trace_reconciled(
757///     base,
758///     &mut rng,
759///     sample(addr!("y"), Normal::new(0.0, 1.0).unwrap()),
760/// ).unwrap();
761/// assert_eq!(report.fresh_addresses, vec![addr!("y")]);
762/// assert_eq!(report.vanished_addresses, vec![addr!("x")]);
763/// assert!(trace.log_prior.is_finite());
764/// ```
765pub fn score_given_trace_reconciled<A, R: RngCore>(
766    base: Trace,
767    rng: &mut R,
768    model: Model<A>,
769) -> FugueResult<(A, Trace, ReconcileReport)> {
770    let mut error: Option<FugueError> = None;
771    let mut fresh_addresses: Vec<Address> = Vec::new();
772    // Snapshot base addresses up front so we can compute vanished ones after the
773    // run consumes the handler.
774    let base_addresses: Vec<Address> = base.choices.keys().cloned().collect();
775    let handler = ReconcilingScoreGivenTrace {
776        rng,
777        base,
778        trace: Trace::default(),
779        fresh: &mut fresh_addresses,
780        error: &mut error,
781    };
782    let (a, trace) = run(handler, model);
783    if let Some(e) = error {
784        return Err(e);
785    }
786    // Vanished = present in the base trace but not visited by this model run.
787    let vanished_addresses: Vec<Address> = base_addresses
788        .into_iter()
789        .filter(|addr| !trace.choices.contains_key(addr))
790        .collect();
791    Ok((
792        a,
793        trace,
794        ReconcileReport {
795            fresh_addresses,
796            vanished_addresses,
797        },
798    ))
799}
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804    use crate::addr;
805    use crate::core::distribution::*;
806    use crate::core::model::{observe, sample, ModelExt};
807    use rand::rngs::StdRng;
808    use rand::SeedableRng;
809
810    #[test]
811    fn prior_handler_samples_and_accumulates() {
812        let mut rng = StdRng::seed_from_u64(7);
813        let (_val, trace) = crate::runtime::handler::run(
814            PriorHandler {
815                rng: &mut rng,
816                trace: Trace::default(),
817            },
818            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
819                .and_then(|x| observe(addr!("y"), Normal::new(x, 1.0).unwrap(), 0.5)),
820        );
821        assert!(trace.choices.contains_key(&addr!("x")));
822        assert!(trace.log_prior.is_finite());
823        assert!(trace.log_likelihood.is_finite());
824    }
825
826    #[test]
827    fn replay_handler_reuses_values() {
828        let mut rng = StdRng::seed_from_u64(8);
829        let ((), base) = crate::runtime::handler::run(
830            PriorHandler {
831                rng: &mut rng,
832                trace: Trace::default(),
833            },
834            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()).map(|_| ()),
835        );
836
837        let ((), replayed) = crate::runtime::handler::run(
838            ReplayHandler {
839                rng: &mut rng,
840                base: base.clone(),
841                trace: Trace::default(),
842            },
843            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()).map(|_| ()),
844        );
845
846        let x_base = base.get_f64(&addr!("x")).unwrap();
847        let x_replay = replayed.get_f64(&addr!("x")).unwrap();
848        assert_eq!(x_base, x_replay);
849    }
850
851    #[test]
852    fn score_given_trace_scores_fixed_values() {
853        let mut rng = StdRng::seed_from_u64(9);
854        let (_a, base) = crate::runtime::handler::run(
855            PriorHandler {
856                rng: &mut rng,
857                trace: Trace::default(),
858            },
859            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
860        );
861
862        let (_a2, scored) = crate::runtime::handler::run(
863            ScoreGivenTrace {
864                base: base.clone(),
865                trace: Trace::default(),
866            },
867            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
868        );
869
870        // Should have same value and finite log_prior
871        assert_eq!(scored.get_f64(&addr!("x")), base.get_f64(&addr!("x")));
872        assert!(scored.log_prior.is_finite());
873    }
874
875    #[test]
876    fn safe_variants_handle_mismatches() {
877        // Build base trace with x as f64, then attempt to replay as bool
878        let mut rng = StdRng::seed_from_u64(10);
879        let (_a, base) = crate::runtime::handler::run(
880            PriorHandler {
881                rng: &mut rng,
882                trace: Trace::default(),
883            },
884            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
885        );
886
887        // SafeReplayHandler should sample fresh value for bool and continue
888        let (_b, t1) = crate::runtime::handler::run(
889            SafeReplayHandler {
890                rng: &mut rng,
891                base: base.clone(),
892                trace: Trace::default(),
893                warn_on_mismatch: true,
894            },
895            sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
896        );
897        assert!(t1.log_prior.is_finite());
898
899        // SafeScoreGivenTrace should mark as invalid by adding -inf
900        let (_c, t2) = crate::runtime::handler::run(
901            SafeScoreGivenTrace {
902                base: base.clone(),
903                trace: Trace::default(),
904                warn_on_error: true,
905            },
906            sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
907        );
908        assert!(t2.log_prior.is_infinite());
909    }
910
911    #[test]
912    fn handlers_cover_all_types_sample_and_observe() {
913        // Model with multiple types
914        let model = sample(addr!("f"), Normal::new(0.0, 1.0).unwrap())
915            .and_then(|_| sample(addr!("b"), Bernoulli::new(0.6).unwrap()))
916            .and_then(|_| sample(addr!("u64"), Poisson::new(3.0).unwrap()))
917            .and_then(|_| sample(addr!("usz"), Categorical::new(vec![0.3, 0.7]).unwrap()))
918            .and_then(|_| observe(addr!("f_obs"), Normal::new(0.0, 1.0).unwrap(), 0.1))
919            .and_then(|_| observe(addr!("b_obs"), Bernoulli::new(0.4).unwrap(), true))
920            .and_then(|_| observe(addr!("u64_obs"), Poisson::new(2.0).unwrap(), 1))
921            .and_then(|_| {
922                observe(
923                    addr!("usz_obs"),
924                    Categorical::new(vec![0.5, 0.5]).unwrap(),
925                    1,
926                )
927            });
928
929        let (_a, t) = crate::runtime::handler::run(
930            PriorHandler {
931                rng: &mut StdRng::seed_from_u64(100),
932                trace: Trace::default(),
933            },
934            model,
935        );
936        assert!(t.get_f64(&addr!("f")).is_some());
937        assert!(t.get_bool(&addr!("b")).is_some());
938        assert!(t.get_u64(&addr!("u64")).is_some());
939        assert!(t.get_usize(&addr!("usz")).is_some());
940        assert!(t.log_likelihood.is_finite());
941
942        // Build base and score given trace for all types
943        let base = t.clone();
944        let (_sv, scored) = crate::runtime::handler::run(
945            ScoreGivenTrace {
946                base: base.clone(),
947                trace: Trace::default(),
948            },
949            sample(addr!("f"), Normal::new(0.0, 1.0).unwrap())
950                .and_then(|_| sample(addr!("b"), Bernoulli::new(0.6).unwrap()))
951                .and_then(|_| sample(addr!("u64"), Poisson::new(3.0).unwrap()))
952                .and_then(|_| sample(addr!("usz"), Categorical::new(vec![0.3, 0.7]).unwrap())),
953        );
954        assert!(scored.log_prior.is_finite());
955
956        // Safe replay mismatches for integer/categorical types
957        let (_sv2, safe) = crate::runtime::handler::run(
958            SafeReplayHandler {
959                rng: &mut StdRng::seed_from_u64(101),
960                base: base.clone(),
961                trace: Trace::default(),
962                warn_on_mismatch: true,
963            },
964            sample(addr!("u64"), Bernoulli::new(0.5).unwrap()),
965        );
966        assert!(safe.log_prior.is_finite());
967    }
968
969    #[test]
970    fn safe_score_given_trace_warn_flag_branches() {
971        let mut rng = StdRng::seed_from_u64(102);
972        let (_a, base) = crate::runtime::handler::run(
973            PriorHandler {
974                rng: &mut rng,
975                trace: Trace::default(),
976            },
977            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
978        );
979        // warn_on_error = false
980        let (_b, t_false) = crate::runtime::handler::run(
981            SafeScoreGivenTrace {
982                base: base.clone(),
983                trace: Trace::default(),
984                warn_on_error: false,
985            },
986            sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
987        );
988        assert!(t_false.log_prior.is_infinite());
989
990        // warn_on_error = true
991        let (_c, t_true) = crate::runtime::handler::run(
992            SafeScoreGivenTrace {
993                base: base.clone(),
994                trace: Trace::default(),
995                warn_on_error: true,
996            },
997            sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
998        );
999        assert!(t_true.log_prior.is_infinite());
1000    }
1001
1002    #[test]
1003    #[should_panic]
1004    fn replay_handler_panics_on_type_mismatch() {
1005        // Base has f64, replay expects bool -> panic as designed
1006        let mut rng = StdRng::seed_from_u64(103);
1007        let (_a, base) = crate::runtime::handler::run(
1008            PriorHandler {
1009                rng: &mut rng,
1010                trace: Trace::default(),
1011            },
1012            sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
1013        );
1014        let (_b, _t) = crate::runtime::handler::run(
1015            ReplayHandler {
1016                rng: &mut rng,
1017                base: base.clone(),
1018                trace: Trace::default(),
1019            },
1020            sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
1021        );
1022    }
1023
1024    #[test]
1025    fn safe_replay_handler_samples_fresh_for_missing_address() {
1026        let mut rng = StdRng::seed_from_u64(104);
1027        // Base trace without address "z"
1028        let base = Trace::default();
1029        let (_a, t) = crate::runtime::handler::run(
1030            SafeReplayHandler {
1031                rng: &mut rng,
1032                base,
1033                trace: Trace::default(),
1034                warn_on_mismatch: true,
1035            },
1036            sample(addr!("z"), Normal::new(0.0, 1.0).unwrap()),
1037        );
1038        assert!(t.get_f64(&addr!("z")).is_some());
1039    }
1040}