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}