1pub mod analytic;
5pub mod builtins;
6pub mod chain;
7pub mod complex;
8pub mod conjugate;
9pub mod continuous;
10pub mod data;
11pub mod dates;
12pub mod dist;
13pub mod error;
14pub mod failure;
15pub mod interp;
16mod math;
17pub mod ops;
18mod ordering;
19pub mod report;
20mod stats;
21mod text;
22mod type_name;
23pub mod value;
24pub mod weight;
25pub mod world;
26
27pub use error::{ErrorKind, Fault, RuntimeError};
28pub use failure::{Failure, Failures};
29pub use interp::{Stats, Updates};
30pub use probl_sema::ir::FailureMode;
31pub use weight::Weight;
32
33use continuous::Rng;
34use dist::Budget;
35use error::OpError;
36use probl_sema::Liveness;
37use probl_sema::conjugate::{Conjugacy, Variable};
38use probl_sema::ir::{Mode, Program};
39use probl_syntax::Span;
40use report::{Format, Sink};
41use std::any::Any;
42use std::collections::BTreeMap;
43use std::panic::AssertUnwindSafe;
44use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
45use std::sync::{Arc, Condvar, Mutex, PoisonError, mpsc};
46use value::Value;
47
48#[derive(Clone, Debug)]
51pub struct Limits {
52 pub max_integer_bits: u64,
54 pub max_integer_bytes: u64,
56 pub max_string_bytes: usize,
58 pub max_string_alloc_bytes: u64,
60 pub max_worlds: usize,
62 pub max_outcomes: usize,
64 pub max_collection: usize,
66 pub max_work: u64,
68 pub max_iterations: u64,
70 pub max_call_depth: usize,
72 pub max_cached_calls: usize,
74 pub max_output: usize,
76 pub stack_size: usize,
79 pub max_threads: usize,
82 pub max_chain_states: usize,
85}
86
87impl Default for Limits {
88 fn default() -> Limits {
89 Limits {
90 max_integer_bits: probl_number::MAX_INTEGER_BITS,
91 max_integer_bytes: 256 * 1024 * 1024,
92 max_string_bytes: 16 * 1024 * 1024,
93 max_string_alloc_bytes: 256 * 1024 * 1024,
94 max_worlds: 10_000_000,
95 max_outcomes: 2_000_000,
96 max_collection: 10_000_000,
97 max_work: 20_000_000_000,
98 max_iterations: 10_000_000,
99 max_call_depth: 500,
100 max_cached_calls: 1_000_000,
101 max_output: 64 * 1024 * 1024,
102 stack_size: 64 * 1024 * 1024,
103 max_threads: std::thread::available_parallelism().map_or(1, |n| n.get()),
104 max_chain_states: 50_000,
105 }
106 }
107}
108
109#[derive(Clone, Debug)]
112pub struct Options {
113 pub today: Option<i32>,
117 pub merge: bool,
120 pub memoize: bool,
122 pub epsilon: Option<f64>,
124 pub fractions: bool,
126 pub limits: Limits,
127 pub cancel: Option<Arc<AtomicBool>>,
129 pub mode: Option<Mode>,
131 pub inputs: Option<Arc<data::Inputs>>,
134 pub conjugate: bool,
138 pub solve: bool,
142 pub progress: Option<Progress>,
145 pub on_error: Option<FailureMode>,
149}
150
151#[derive(Clone)]
153pub struct Progress(pub Arc<dyn Fn(u64, u64) + Send + Sync>);
154
155impl std::fmt::Debug for Progress {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 f.write_str("Progress")
158 }
159}
160
161impl Default for Options {
162 fn default() -> Options {
163 Options {
164 today: None,
165 merge: true,
166 memoize: true,
167 epsilon: None,
168 fractions: false,
169 limits: Limits::default(),
170 cancel: None,
171 mode: None,
172 inputs: None,
173 conjugate: true,
174 solve: true,
175 progress: None,
176 on_error: None,
177 }
178 }
179}
180
181#[derive(Clone, Debug)]
182pub struct Outcome {
183 pub today: Option<i32>,
185 pub output: String,
187 pub stats: Stats,
188 pub unresolved: Weight,
190 pub evidence: Option<Weight>,
192 pub reports: Vec<Sink>,
194 pub results: Vec<report::ReportResult>,
196 pub format: Format,
199 pub sample: Option<Sampled>,
201 pub data: Vec<data::SourceInfo>,
203 pub updates: Vec<(Variable, Updates)>,
206 pub on_error: FailureMode,
209 pub failures: Failures,
212 pub finished: Weight,
215}
216
217#[derive(Clone, Debug)]
219pub struct Sampled {
220 pub runs: u64,
221 pub seed: u64,
222 pub effective: f64,
224 pub weight: Weight,
226 pub squares: Weight,
228 pub evidence_se: f64,
231 pub densities: bool,
234}
235
236pub fn run(
240 program: &Program,
241 options: &Options,
242 print: &mut (dyn FnMut(&str) + Send),
243) -> Result<Outcome, RuntimeError> {
244 std::thread::scope(|scope| {
245 let handle = std::thread::Builder::new()
246 .name("probl-engine".into())
247 .stack_size(options.limits.stack_size)
248 .spawn_scoped(scope, || run_here(program, options, print));
249 let handle = match handle {
250 Ok(h) => h,
251 Err(e) => {
252 return Err(internal(format!("couldn't start the engine: {e}")));
253 }
254 };
255 handle
256 .join()
257 .unwrap_or_else(|panic| Err(internal(panic_detail(&*panic))))
258 })
259}
260
261pub fn run_on_this_thread(
268 program: &Program,
269 options: &Options,
270 print: &mut (dyn FnMut(&str) + Send),
271) -> Result<Outcome, RuntimeError> {
272 run_here(program, options, print)
273}
274
275fn panic_detail(panic: &(dyn Any + Send)) -> String {
277 panic
278 .downcast_ref::<String>()
279 .cloned()
280 .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
281 .unwrap_or_else(|| "no details".to_string())
282}
283
284fn internal(detail: String) -> RuntimeError {
285 OpError::internal("internal error: the engine crashed")
286 .at(Span::default())
287 .with_note(detail)
288}
289
290fn run_here(
291 program: &Program,
292 options: &Options,
293 print: &mut (dyn FnMut(&str) + Send),
294) -> Result<Outcome, RuntimeError> {
295 if options.today.is_some_and(|d| !dates::valid(d)) {
296 return Err(RuntimeError::new(
297 Default::default(),
298 "execution date must be within 0001-01-01..9999-12-31",
299 ));
300 }
301 let settings = &program.settings;
302 let mode = options.mode.clone().unwrap_or_else(|| settings.mode.clone());
303 let sample = match mode {
304 Mode::Auto | Mode::Enumerate => None,
305 Mode::Sample { runs, seed } => Some((runs, seed)),
306 ref other => {
307 return Err(
308 OpError::unsupported(format!("`{}` mode isn't implemented yet", other.name()))
309 .help("enumeration and `@mode sample(runs: 10_000)` are; particles and beam search come later")
310 .at(settings.mode_span.unwrap_or_default()),
311 );
312 }
313 };
314 if let Some((runs, _)) = sample {
315 if runs == 0 || runs > u32::MAX as u64 {
316 return Err(OpError::new(format!(
317 "sample mode needs between 1 and {} runs",
318 report::thousands(u32::MAX as i64)
319 ))
320 .at(settings.mode_span.unwrap_or_default()));
321 }
322 }
323 let on_error = options.on_error.or(settings.on_error).unwrap_or(match sample {
326 Some(_) => FailureMode::Partial,
327 None => FailureMode::Total,
328 });
329 let limits = &options.limits;
330 let config = interp::Config {
331 today: options.today,
332 epsilon: options.epsilon.unwrap_or(settings.epsilon),
333 merging: options.merge,
334 memoizing: options.memoize,
335 max_worlds: limits.max_worlds.min(settings.max_worlds),
336 max_iterations: limits.max_iterations.min(settings.max_iterations),
337 max_call_depth: limits.max_call_depth,
338 max_cached_calls: limits.max_cached_calls,
339 max_output: limits.max_output,
340 budget: Budget {
341 max_integer_bits: limits.max_integer_bits,
342 integer_bytes_left: Arc::new(AtomicU64::new(limits.max_integer_bytes)),
343 max_string_bytes: limits.max_string_bytes,
344 string_bytes_left: Arc::new(AtomicU64::new(limits.max_string_alloc_bytes)),
345 cancel: options.cancel.clone(),
346 max_outcomes: limits.max_outcomes,
347 max_collection: limits.max_collection,
348 work_left: limits.max_work,
349 shared: None,
350 },
351 cancel: options.cancel.clone(),
352 sample_seed: sample.map(|(_, seed)| seed),
353 conjugate: options.conjugate,
354 solving: options.solve,
355 max_chain_states: limits.max_chain_states,
356 partial: on_error == FailureMode::Partial,
357 };
358 let epsilon = config.epsilon;
359 let inputs = inputs(program, options)?;
360 let live = probl_sema::analyze(program);
361 let conj = probl_sema::conjugate::analyze(program);
362 if let Some((runs, seed)) = sample {
363 return sampled(program, &live, &conj, config, options, print, runs, seed, on_error);
364 }
365 let mut engine = interp::Engine::new(program, &live, &conj, config, inputs, print);
366 let finished = engine.run_main()?;
367 let unresolved = engine.unresolved;
368 let failures = std::mem::take(&mut engine.failures);
369
370 if engine.observed && finished.is_zero() && failures.is_empty() {
371 let span = engine.last_ruling_out.unwrap_or_default();
372 return Err(if unresolved.is_zero() {
373 RuntimeError::new(
374 span,
375 "the evidence is impossible: every world was ruled out by `observe`",
376 )
377 .with_help("check the observations; there's no answer to condition on")
378 } else {
379 RuntimeError::new(span, "every world that fits the evidence was left unresolved")
380 .with_help("lower `@epsilon` so that loops run longer")
381 });
382 }
383
384 let comparable = !failures.before_evidence;
388 let total = if comparable {
389 finished + failures.weight
390 } else {
391 finished
392 };
393 let format = Format {
394 fractions: options.fractions,
395 weighted: program.main().effects.observes,
396 unresolved,
397 program_total: total,
398 run_squares: None,
399 reach_known: comparable,
400 };
401 let plain = Format {
402 fractions: false,
403 ..format
404 };
405 let mut header = String::from("enumerated");
406 if !failures.is_empty() {
407 header.push_str(" · partial result");
408 if comparable && !total.is_zero() {
409 header.push_str(&format!(
410 " · {} failed",
411 report::pct(failures.weight.ratio(total), plain)
412 ));
413 }
414 }
415 let evidence = engine.observed.then_some(total);
418 let of = if comparable { "" } else { " of the finished worlds" };
419 if let Some(z) = evidence {
420 let (lo, hi) = (total.to_f64(), (total + unresolved).to_f64());
421 if hi - lo >= 0.00005 {
422 header.push_str(&format!(
423 " · evidence{of} {}–{}",
424 report::pct(lo, plain),
425 report::pct(hi, plain)
426 ));
427 } else if z.to_f64() < 0.0001 {
428 header.push_str(&format!(" · evidence{of} {}", scientific(z)));
429 } else {
430 header.push_str(&format!(" · evidence{of} {}", report::pct(z.to_f64(), plain)));
431 }
432 }
433 if !unresolved.is_zero() {
434 if unresolved.to_f64() <= epsilon {
435 header.push_str(&format!(" · unresolved < {epsilon:e}"));
436 } else {
437 header.push_str(&format!(" · unresolved {:.1e}", unresolved.to_f64()));
438 }
439 }
440 let results = report::results(&program.reports, &engine.sinks, format, unresolved);
441 let body = report::render_results(&program.reports, &results, format);
442 let output = if body.is_empty() {
443 header
444 } else {
445 format!("{header}\n\n{body}")
446 };
447 Ok(Outcome {
448 today: options.today,
449 output,
450 stats: engine.stats.clone(),
451 unresolved,
452 evidence,
453 reports: std::mem::take(&mut engine.sinks),
454 results,
455 format,
456 sample: None,
457 data: sources(options),
458 updates: Vec::new(),
459 on_error,
460 failures,
461 finished,
462 })
463}
464
465fn scientific(w: Weight) -> String {
467 let l = w.log10();
468 let exponent = l.floor();
469 let mantissa = libm::pow(10.0, l - exponent);
470 let (mantissa, exponent) = if format!("{mantissa:.2}") == "10.00" {
472 (1.0, exponent + 1.0)
473 } else {
474 (mantissa, exponent)
475 };
476 format!("{mantissa:.2}e{exponent}")
477}
478
479fn evidence_estimate(z: Weight, relative_se: f64, densities: bool) -> String {
482 let known = relative_se.is_finite();
483 if densities {
484 let ln = z.log10() * std::f64::consts::LN_10;
485 if !known {
486 return format!("log evidence {ln:.2}");
487 }
488 let decimals = if relative_se > 0.0 {
489 (-libm::log10(relative_se).floor()).clamp(2.0, 6.0) as usize
490 } else {
491 2
492 };
493 return format!("log evidence {ln:.decimals$} ± {relative_se:.decimals$}");
494 }
495 let x = z.to_f64();
496 if x >= 1e-4 {
497 return match known {
498 true => format!("evidence {}", report::estimate(x, x * relative_se)),
499 false => format!("evidence {}", value::fmt_prob(x)),
500 };
501 }
502 if !known {
503 return format!("evidence {}", scientific(z));
504 }
505 let pct = relative_se * 100.0;
506 let decimals = if pct >= 10.0 {
507 0
508 } else if pct >= 1.0 {
509 1
510 } else {
511 2
512 };
513 format!("evidence {} (± {pct:.decimals$}%)", scientific(z))
514}
515
516fn inputs<'a>(program: &Program, options: &'a Options) -> Result<&'a [Value], RuntimeError> {
518 let Some(first) = program.inputs.first() else {
519 return Ok(&[]);
520 };
521 match &options.inputs {
522 Some(inputs) if inputs.fit(program) => {
523 if inputs.max_string_bytes() > options.limits.max_string_bytes {
524 return Err(RuntimeError::limit(
525 first.span,
526 "an input string exceeds the runtime string size limit",
527 ));
528 }
529 if inputs.max_integer_bits() > options.limits.max_integer_bits {
530 return Err(RuntimeError::limit(
531 first.span,
532 "an input integer exceeds the runtime integer size limit",
533 ));
534 }
535 Ok(inputs.values())
536 }
537 Some(_) => Err(internal("the data was loaded for a different program".to_string())),
538 None => Err(
539 RuntimeError::new(first.span, "the program reads data, which wasn't loaded")
540 .with_help("load it with `probl_engine::data::load` before running the program"),
541 ),
542 }
543}
544
545fn sources(options: &Options) -> Vec<data::SourceInfo> {
546 options.inputs.as_ref().map_or_else(Vec::new, |i| i.sources().to_vec())
547}
548
549struct Combined {
551 sinks: Vec<Sink>,
552 totals: interp::SampleTotals,
553 observed: bool,
554 densities: bool,
555 unresolved: Weight,
556 last_ruling_out: Option<Span>,
557 stats: Stats,
558 failures: Failures,
559}
560
561impl Combined {
562 fn new(reports: usize) -> Combined {
563 Combined {
564 sinks: vec![Sink::default(); reports],
565 totals: interp::SampleTotals {
566 weight: Weight::ZERO,
567 squares: Weight::ZERO,
568 },
569 observed: false,
570 densities: false,
571 unresolved: Weight::ZERO,
572 last_ruling_out: None,
573 stats: Stats::default(),
574 failures: Failures::default(),
575 }
576 }
577
578 fn absorb(&mut self, batch: interp::Batch) {
579 for (sink, theirs) in self.sinks.iter_mut().zip(batch.sinks) {
580 sink.absorb(theirs);
581 }
582 self.totals.weight += batch.totals.weight;
583 self.totals.squares += batch.totals.squares;
584 self.observed |= batch.observed;
585 self.densities |= batch.densities;
586 self.unresolved += batch.unresolved;
587 self.last_ruling_out = batch.last_ruling_out.or(self.last_ruling_out);
588 self.stats.absorb(&batch.stats);
589 self.failures.append(batch.failures);
590 }
591}
592
593type Sent = (u64, Result<interp::Batch, RuntimeError>, interp::Printed);
596
597#[allow(clippy::too_many_arguments)]
602fn run_batches(
603 program: &Program,
604 live: &Liveness,
605 conj: &Conjugacy,
606 config: &interp::Config,
607 options: &Options,
608 print: &mut (dyn FnMut(&str) + Send),
609 runs: u64,
610 seed: u64,
611) -> Result<Combined, RuntimeError> {
612 let batches = runs.div_ceil(interp::BATCH);
613 let threads = (options.limits.max_threads.max(1) as u64).min(batches);
614 let inputs = inputs(program, options)?;
615 let ahead = 2 * threads;
618 let mut config = config.clone();
619 config.budget.shared = Some(Arc::new(AtomicU64::new(config.budget.work_left)));
620 config.budget.work_left = 0;
621 let config = &config;
622 if threads == 1 {
623 return run_batches_here(program, live, conj, config, inputs, options, print, runs, seed);
624 }
625
626 let next = AtomicU64::new(0);
627 let failed = AtomicU64::new(u64::MAX);
629 let printed = AtomicUsize::new(0);
631 let combined_upto = Mutex::new(0u64);
633 let caught_up = Condvar::new();
634 let stop = |error: RuntimeError| {
635 let _upto = combined_upto.lock().unwrap_or_else(PoisonError::into_inner);
637 failed.store(0, Ordering::Relaxed);
638 caught_up.notify_all();
639 Err(error)
640 };
641 let (tx, rx) = mpsc::channel::<Sent>();
642 std::thread::scope(|scope| {
643 for t in 0..threads {
644 let tx = tx.clone();
645 let (next, failed, printed) = (&next, &failed, &printed);
646 let (combined_upto, caught_up) = (&combined_upto, &caught_up);
647 let worker = move || {
648 let mut ignore = |_: &str| {};
649 let copied: Vec<Value> = inputs.iter().map(Value::unshared).collect();
652 let mut engine = interp::Engine::new(program, live, conj, config.clone(), &copied, &mut ignore);
653 loop {
654 let index = next.fetch_add(1, Ordering::Relaxed);
655 if index >= batches {
656 break;
657 }
658 let mut upto = combined_upto.lock().unwrap_or_else(PoisonError::into_inner);
659 while index >= *upto + ahead && index <= failed.load(Ordering::Relaxed) {
660 upto = caught_up.wait(upto).unwrap_or_else(PoisonError::into_inner);
661 }
662 drop(upto);
663 if index > failed.load(Ordering::Relaxed) {
664 break;
665 }
666 let first = index * interp::BATCH;
667 let n = (runs - first).min(interp::BATCH);
668 let before = printed.load(Ordering::Relaxed);
669 let (result, lines) = std::panic::catch_unwind(AssertUnwindSafe(|| {
670 engine.run_batch(Rng::stream(seed, index), first, n, before)
671 }))
672 .unwrap_or_else(|panic| (Err(internal(panic_detail(&*panic))), Vec::new()));
673 let stopped = result.is_err();
674 if stopped {
675 failed.fetch_min(index, Ordering::Relaxed);
676 }
677 if tx.send((index, result, lines)).is_err() || stopped {
678 break;
679 }
680 }
681 };
682 let spawned = std::thread::Builder::new()
683 .name("probl-sampler".into())
684 .stack_size(options.limits.stack_size)
685 .spawn_scoped(scope, worker);
686 if let Err(e) = spawned {
687 if t == 0 {
689 return Err(internal(format!("couldn't start a sampling thread: {e}")));
690 }
691 break;
692 }
693 }
694 drop(tx);
695
696 let mut combined = Combined::new(program.reports.len());
697 let mut pending = BTreeMap::new();
698 let mut bytes = 0;
699 for index in 0..batches {
700 let (result, lines) = loop {
701 if let Some(sent) = pending.remove(&index) {
702 break sent;
703 }
704 match rx.recv() {
705 Ok((i, result, lines)) => {
706 pending.insert(i, (result, lines));
707 }
708 Err(_) => return stop(internal("a sampling thread stopped before its batch was done".into())),
709 }
710 };
711 for (span, line) in lines {
712 bytes += line.len() + 1;
713 if bytes > options.limits.max_output {
714 return stop(interp::too_much_output(span));
715 }
716 print(&line);
717 }
718 printed.store(bytes, Ordering::Relaxed);
719 match result {
720 Ok(batch) => combined.absorb(batch),
721 Err(e) => return stop(e),
722 }
723 if let Some(progress) = &options.progress {
724 (progress.0)(((index + 1) * interp::BATCH).min(runs), runs);
725 }
726 *combined_upto.lock().unwrap_or_else(PoisonError::into_inner) = index + 1;
727 caught_up.notify_all();
728 }
729 Ok(combined)
730 })
731}
732
733#[allow(clippy::too_many_arguments)]
736fn run_batches_here(
737 program: &Program,
738 live: &Liveness,
739 conj: &Conjugacy,
740 config: &interp::Config,
741 inputs: &[Value],
742 options: &Options,
743 print: &mut (dyn FnMut(&str) + Send),
744 runs: u64,
745 seed: u64,
746) -> Result<Combined, RuntimeError> {
747 let mut ignore = |_: &str| {};
748 let mut engine = interp::Engine::new(program, live, conj, config.clone(), inputs, &mut ignore);
749 let mut combined = Combined::new(program.reports.len());
750 let mut bytes = 0;
751 for index in 0..runs.div_ceil(interp::BATCH) {
752 let first = index * interp::BATCH;
753 let n = (runs - first).min(interp::BATCH);
754 let (result, lines) = engine.run_batch(Rng::stream(seed, index), first, n, bytes);
755 for (span, line) in lines {
756 bytes += line.len() + 1;
757 if bytes > options.limits.max_output {
758 return Err(interp::too_much_output(span));
759 }
760 print(&line);
761 }
762 combined.absorb(result?);
763 if let Some(progress) = &options.progress {
764 (progress.0)(first + n, runs);
765 }
766 }
767 Ok(combined)
768}
769
770#[allow(clippy::too_many_arguments)]
772fn sampled(
773 program: &Program,
774 live: &Liveness,
775 conj: &Conjugacy,
776 config: interp::Config,
777 options: &Options,
778 print: &mut (dyn FnMut(&str) + Send),
779 runs: u64,
780 seed: u64,
781 on_error: FailureMode,
782) -> Result<Outcome, RuntimeError> {
783 let mut engine = run_batches(program, live, conj, &config, options, print, runs, seed)?;
784 let totals = engine.totals;
785 let failures = std::mem::take(&mut engine.failures);
786 if totals.weight.is_zero() && failures.is_empty() {
787 let span = engine.last_ruling_out.unwrap_or_default();
788 return Err(RuntimeError::new(span, "every run was ruled out by `observe`")
789 .with_note(format!("{} runs were tried", report::thousands(runs as i64)))
790 .with_help(
791 "the evidence may be impossible, or too unlikely for this many runs: enumerate the model, or add runs",
792 ));
793 }
794 let effective = (totals.weight * totals.weight).ratio(totals.squares);
795 let mut header = format!("sample · {} runs · seed {seed}", report::thousands(runs as i64));
796 if !failures.is_empty() {
797 let failed = match failures.runs {
798 1 => "1 run".to_string(),
799 n => format!("{} runs", report::thousands(n as i64)),
800 };
801 header.push_str(&format!(" · partial result · {failed} failed"));
802 }
803 let comparable = !failures.before_evidence;
806 let (total, total_squares) = match comparable {
807 true => (totals.weight + failures.weight, totals.squares + failures.squares),
808 false => (totals.weight, totals.squares),
809 };
810 let evidence = total.scale(1.0 / runs as f64);
813 let n = runs as f64;
814 let evidence_se = if runs > 1 {
815 let effective = (total * total).ratio(total_squares);
816 ((n / effective - 1.0).max(0.0) / (n - 1.0)).sqrt()
817 } else {
818 f64::NAN
819 };
820 if engine.observed {
821 let of = if comparable { "" } else { " of the finished runs" };
822 header.push_str(&format!(
823 " · {}{of}",
824 evidence_estimate(evidence, evidence_se, engine.densities)
825 ));
826 header.push_str(&format!(
827 " · effective sample size {}",
828 report::thousands(effective.round() as i64)
829 ));
830 }
831 let unresolved = engine.unresolved;
832 if !unresolved.is_zero() {
833 header.push_str(&format!(
834 " · unresolved {:.1e}",
835 unresolved.ratio(totals.weight + unresolved)
836 ));
837 }
838 let format = Format {
839 fractions: false,
840 unresolved: Weight::ZERO,
841 program_total: total,
842 run_squares: Some(total_squares),
843 weighted: program.main().effects.observes,
844 reach_known: comparable,
845 };
846 let results = report::results(&program.reports, &engine.sinks, format, unresolved);
849 let body = report::render_results(&program.reports, &results, format);
850 let output = if body.is_empty() {
851 header
852 } else {
853 format!("{header}\n\n{body}")
854 };
855 Ok(Outcome {
856 today: options.today,
857 output,
858 stats: engine.stats.clone(),
859 unresolved,
860 evidence: engine.observed.then_some(evidence),
861 reports: std::mem::take(&mut engine.sinks),
862 results,
863 format,
864 sample: Some(Sampled {
865 runs,
866 seed,
867 effective,
868 weight: totals.weight,
869 squares: totals.squares,
870 evidence_se,
871 densities: engine.densities,
872 }),
873 data: sources(options),
874 updates: conj
875 .variables
876 .iter()
877 .enumerate()
878 .map(|(i, v)| (v.clone(), engine.stats.updates.get(i).copied().unwrap_or_default()))
879 .collect(),
880 on_error,
881 failures,
882 finished: totals.weight,
883 })
884}