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