tididi_cnf/
output.rs

1//! What the run answers, and what the command line prints of it.
2//!
3//! Each mode's runner returns what it answered as a typed value — a model
4//! count with its attribution, a weighted count, a projected count with the
5//! fields its stages measured, the compiled diagram — and the driver records
6//! it once, here: the verdict and the count, and the run's record, [`Stats`]:
7//! sizes, timings, which stage answered, the preprocessing and SAT-pruning
8//! tallies, how the run ended. `driver::solve` hands both back as an
9//! [`Answer`], which is what [`crate::count`] returns. `eprintln!` diagnostics
10//! stay with the stage that emits them.
11//!
12//! The command line's half is [`finish`]: stdout carries the answer — the MCC
13//! answer lines under `--output-format mcc`, and otherwise a plainly worded
14//! count and, with `--save-tdd`, the line naming the written file — and
15//! `--stats` writes the record as one JSON object, to stdout after the answer
16//! when its path is `-`.
17//!
18//! `docs/stats.md` is the record's field table.
19
20use std::collections::BTreeMap;
21use std::path::{Path, PathBuf};
22
23use serde::Serialize;
24
25use crate::cli::{Cli, OutputFormat};
26use crate::config::{Config, Mode};
27use crate::problem::WeightedCount;
28use crate::run::{Answer, Count, Failure};
29use crate::Error;
30
31/// Which stage of a model count or a plain compile produced the answer: the
32/// record's `answered_by`, and the word the stage's stderr lines carry.
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub(crate) enum AnsweredBy {
35    /// The compile answered: on the cascade, the first compile, of the Arjun
36    /// reduction when one was kept; on the streaming route, its one compile;
37    /// in the plain compile, the compile on the frontend's vtree.
38    Compile,
39    /// Preprocessing settled the count, so nothing was compiled: the formula
40    /// was refuted, or no variable was left and the count is the lift.
41    Preprocessing,
42    /// The model count's second compile of the Arjun reduction, on a
43    /// `goatd-primal` tree, after one step of the first compile ran past its
44    /// own wall.
45    StallRecompile,
46    /// Support enumeration, the fallback that counts the Arjun-reduced formula
47    /// by enumerating assignments to its independent support.
48    SupportEnumeration,
49    /// The fallback that re-reduces without SBVA, then compiles again.
50    NoSbvaRetry,
51    /// The fallback that compiles the formula Arjun was given, unreduced.
52    UnreducedRetry,
53    /// The fallback that asks Arjun for the reduction it skipped on a
54    /// monotone formula, then compiles the reduction.
55    MonotoneReduction,
56    /// Shannon split assembled the count from cofactors.
57    ShannonSplit,
58    /// The plain compile's second compile, on the hypergraph bisection, after
59    /// the compile on the portfolio's vtree ran out of memory.
60    BisectRetry,
61}
62
63impl AnsweredBy {
64    /// The token recorded in `answered_by`. Treat these as a wire format:
65    /// programs that read the record match on them.
66    pub(crate) fn token(self) -> &'static str {
67        match self {
68            AnsweredBy::Compile => "compile",
69            AnsweredBy::Preprocessing => "preprocessing",
70            AnsweredBy::StallRecompile => "stall-recompile",
71            AnsweredBy::SupportEnumeration => "support-enumeration",
72            AnsweredBy::NoSbvaRetry => "no-sbva-retry",
73            AnsweredBy::UnreducedRetry => "unreduced-retry",
74            AnsweredBy::MonotoneReduction => "monotone-reduction",
75            AnsweredBy::ShannonSplit => "shannon-split",
76            AnsweredBy::BisectRetry => "bisect-retry",
77        }
78    }
79}
80
81/// What became of Arjun's reduction on a run that ran Arjun: the record's
82/// `reduction`, absent when Arjun did not run.
83#[derive(Copy, Clone, Debug, PartialEq, Eq)]
84pub(crate) enum Reduction {
85    /// Arjun's reduction was kept, and the count went through it.
86    Used,
87    /// Arjun ran and no reduction was kept — it gave up inside its budget, or
88    /// its result was discarded — so the count went through the formula it was
89    /// given.
90    Unavailable,
91}
92
93impl Reduction {
94    /// The token recorded in `reduction`.
95    pub(crate) fn token(self) -> &'static str {
96        match self {
97            Reduction::Used => "used",
98            Reduction::Unavailable => "unavailable",
99        }
100    }
101}
102
103// ── The record ───────────────────────────────────────────────────────────────
104
105/// One run, as a harness or a researcher reads it back: the record the
106/// binary's `--stats` writes, as a value.
107///
108/// Field order here is the order the JSON object carries, and the names are a
109/// wire format, the keys of that object. A field is `None` when the run has
110/// nothing to say about it — no nulls in the JSON, and never a `0` standing for
111/// "not measured". Counts are decimal strings because they exceed 64 bits;
112/// everything else is a number, a boolean or a token. Serialize it with
113/// `serde_json` to get the object the binary writes.
114///
115/// The [record reference](https://github.com/Tractables/tididi-cnf/blob/main/docs/stats.md)
116/// tabulates the fields and which modes carry each.
117#[derive(Clone, Debug, Default, Serialize)]
118#[non_exhaustive]
119pub struct Stats {
120    /// `mc`, `wmc`, `pmc`, `pwmc`, or `compile` for a plain compile: the
121    /// [`Mode`] the run resolved to.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub mode: Option<&'static str>,
124    /// How the run ended: `solved`, or the class of the error that stopped it
125    /// (`usage`, `environment`, `stage`, `vtree-spec`, `input`, `artifact`,
126    /// `incomplete`).
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub exit_reason: Option<&'static str>,
129    /// Variables in the parsed formula.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub variables: Option<u32>,
132    /// Clauses in the parsed formula.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub clauses: Option<u64>,
135    /// The vtree construction the run compiles on: the binary's `--vtree`, or
136    /// `portfolio`. `None` when the input is a vitri bundle, which brings its
137    /// vtree.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub vtree_spec: Option<String>,
140    /// The portfolio candidate that won selection, when a portfolio ran. In
141    /// the plain compile it is the vtree the answering compile ran on: the
142    /// portfolio's winner, or the retry's hypergraph bisection. In the model
143    /// count the primary compile and the stall recompile name theirs as they
144    /// start, so a run that gives up names the last of them.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub vtree_candidate: Option<String>,
147
148    /// Whether the formula has a model, as the run found it.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub satisfiable: Option<bool>,
151    /// The integer count, as a decimal string.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub model_count: Option<String>,
154    /// The weighted count, spelled as the answer line spells it: an exact
155    /// rational `<n>/<d>` in the exact domain, `<mantissa>e<exponent>` in the
156    /// bounded log domain.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub weighted_count: Option<String>,
159    /// log10 of the weighted count's magnitude, in the log domain only, and
160    /// only when it is finite — a zero count has no logarithm and says so
161    /// through `weighted_count_sign`.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub weighted_count_log10: Option<f64>,
164    /// Sign of the weighted count in the log domain: `-1`, `0` or `1`.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub weighted_count_sign: Option<i8>,
167
168    /// Nodes in the compiled diagram. Only the plain compile path materializes
169    /// one; the counting paths stream and never build it.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub tdd_nodes: Option<u64>,
172    /// Node-pairs in the compiled diagram.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub tdd_pairs: Option<u64>,
175    /// Widest vtree level of the compiled diagram.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub tdd_width: Option<u64>,
178    /// Largest diagram the compile held at once, in node-pairs.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub peak_pairs: Option<u64>,
181
182    /// Which stage produced the answer: of a model count, `compile`,
183    /// `preprocessing`, `stall-recompile`, `support-enumeration`,
184    /// `no-sbva-retry`, `unreduced-retry`, `monotone-reduction` or
185    /// `shannon-split`; of a plain compile, `compile`, `preprocessing` or
186    /// `bisect-retry`.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub answered_by: Option<&'static str>,
189    /// What became of Arjun's reduction on a model count: `used`, or
190    /// `unavailable` when Arjun ran and no reduction was kept. `None` when
191    /// Arjun did not run.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub reduction: Option<&'static str>,
194
195    /// Wall-clock time in preprocessing, in milliseconds. On the model
196    /// count's cascade it is the simplify chain alone, since Arjun has its
197    /// own key.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub preprocess_ms: Option<u64>,
200    /// Wall-clock time in Arjun, in milliseconds.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub arjun_ms: Option<u64>,
203    /// Wall-clock time building vtrees, over the whole run, in milliseconds.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub vtree_ms: Option<u64>,
206    /// Wall-clock time compiling, in milliseconds, less every phase with its
207    /// own key: preprocessing, Arjun and vtree construction.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub compile_ms: Option<u64>,
210    /// Wall-clock time in the model count's cascade on a kept reduction, in
211    /// milliseconds: the first compile, then any fallbacks and Shannon split,
212    /// vtree construction inside it included.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub cascade_ms: Option<u64>,
215    /// Wall-clock time in backbone detection, in milliseconds.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub backbone_ms: Option<u64>,
218    /// Backbone literals found.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub backbone_found: Option<u64>,
221    /// SAT probes backbone detection spent.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub backbone_probes: Option<u64>,
224    /// Variables in the show set of the formula the projected count was
225    /// answered on, after preprocessing; `None` without a show set.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub show_vars: Option<u64>,
228
229    /// `off` when `TIDIDI_SAT_PRUNE=off` switched the SAT-pruning pass off for
230    /// the run, a diagnostic for paired on/off measurements. Absent when the
231    /// pass is on, the default.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub sat_prune: Option<&'static str>,
234    /// Why the SAT-pruning pass stood down, when it did.
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub sat_prune_disabled_by: Option<&'static str>,
237    /// The SAT-pruning tallies, keyed `sat_prune_<tally>`, each present only when
238    /// it is nonzero. Times end `_us` and are microseconds, the unit the pass
239    /// accumulates them in. The JSON object carries them as top-level keys.
240    #[serde(flatten)]
241    pub sat_prune_tallies: BTreeMap<&'static str, u64>,
242}
243
244/// What a mode answered: whether the formula has a model, its count, and in
245/// the compile mode the diagram the count was read off.
246pub(crate) struct Verdict {
247    pub(crate) satisfiable: bool,
248    pub(crate) count: Count,
249    pub(crate) diagram: Option<tididi::Tdd>,
250}
251
252/// Open the run's record: the facts fixed before the mode runs.
253///
254/// Called once, from the driver, at the point the mode and the vtree
255/// construction are both resolved — which is the last point every mode passes
256/// through. A run refused before then (bad argv, an instance the build will not
257/// answer) has no record; its stderr diagnostic is the whole story.
258pub(crate) fn open_record(config: &Config, formula: &vitri::cnf::CnfFormula) -> Stats {
259    Stats {
260        mode: Some(config.mode.token()),
261        variables: Some(formula.num_vars()),
262        clauses: Some(formula.clauses().len() as u64),
263        // A bundle's run builds no vtree for its main compile.
264        vtree_spec: config.input_vtree.is_none().then(|| config.vtree_spec().to_owned()),
265        sat_prune: (!config.sat_prune).then_some("off"),
266        ..Stats::default()
267    }
268}
269
270/// How the run ended, as the record names it: `solved`, or the error's class.
271pub(crate) fn exit_reason<T>(outcome: &Result<T, Error>) -> &'static str {
272    match outcome {
273        Ok(_) => "solved",
274        Err(error) => error.kind(),
275    }
276}
277
278/// Close the run's record once its mode has returned.
279///
280/// The last facts land here rather than with the answer, because they are
281/// the run's and not any one stage's: how the run ended, the time its vtree
282/// constructions took (`vtree_time`), the tree its main compile last started
283/// on (`started_on`), which names the run's `vtree_candidate` when no answer
284/// named one, and the SAT-pruning tallies of its compiles (`sat_prune`).
285/// Recording each in one place is also what keeps the record of a run that
286/// gave up carrying them.
287pub(crate) fn close_record(
288    stats: &mut Stats,
289    outcome: &Result<Verdict, Error>,
290    vtree_time: Option<std::time::Duration>,
291    started_on: Option<String>,
292    sat_prune: Option<crate::compile::sat_prune::SatPruneStats>,
293) {
294    stats.exit_reason = Some(exit_reason(outcome));
295    // A stage can finish after the whole-run deadline, and its answer reach
296    // the record before the driver rejects it. Keep diagnostics, never that
297    // answer.
298    if outcome.is_err() {
299        stats.satisfiable = None;
300        stats.model_count = None;
301        stats.weighted_count = None;
302        stats.weighted_count_log10 = None;
303        stats.weighted_count_sign = None;
304        stats.answered_by = None;
305    }
306    stats.vtree_ms = vtree_time.map(|time| time.as_millis() as u64);
307    if stats.vtree_candidate.is_none() {
308        stats.vtree_candidate = started_on;
309    }
310    // Each tally is recorded only when it is nonzero: presence is the "this
311    // happened" signal. None is there when no pass ran to its end.
312    if let Some(prune) = sat_prune {
313        stats.sat_prune_tallies = prune.tallies().filter(|(_, v)| *v > 0).collect();
314        stats.sat_prune_disabled_by = prune.disabled_by.map(crate::compile::sat_prune::PruneStop::key);
315    }
316}
317
318// ── The answers ──────────────────────────────────────────────────────────────
319
320/// Write the verdict and the count into the record, as the count's domain
321/// spells it, and hand the verdict back.
322fn recorded(stats: &mut Stats, satisfiable: bool, count: Count, diagram: Option<tididi::Tdd>) -> Verdict {
323    stats.satisfiable = Some(satisfiable);
324    match &count {
325        Count::Integer(n) => stats.model_count = Some(n.to_string()),
326        Count::Rational(_) => stats.weighted_count = Some(count.to_string()),
327        Count::Log(s) => {
328            // A zero count has no logarithm; the sign says so instead.
329            let log10 = s.log10_abs();
330            stats.weighted_count_log10 = log10.is_finite().then_some(log10);
331            stats.weighted_count_sign = Some(s.sign);
332            stats.weighted_count = Some(count.to_string());
333        }
334    }
335    Verdict { satisfiable, count, diagram }
336}
337
338/// [`recorded`] for an integer count: a model count is zero exactly when the
339/// formula has no model, so the count is the verdict.
340fn recorded_integer(stats: &mut Stats, count: num_bigint::BigUint, diagram: Option<tididi::Tdd>) -> Verdict {
341    let satisfiable = count != num_bigint::BigUint::ZERO;
342    recorded(stats, satisfiable, Count::Integer(count), diagram)
343}
344
345/// The portfolio candidate that won the run's last selection, recorded with
346/// the model and weighted counts' answers, since only by then has selection
347/// run. The plain compile names the tree its answering compile ran on
348/// instead, the last one it started on ([`close_record`]). The spec itself is
349/// settled before the mode dispatches and is recorded once, in
350/// [`open_record`].
351fn record_vtree_candidate(stats: &mut Stats, config: &Config) {
352    if let Some(candidate) = config.portfolio_history.last_winning_spec() {
353        stats.vtree_candidate = Some(candidate);
354    }
355}
356
357/// Record a model count with what the model count and the compile report
358/// alongside it.
359fn record_integer(
360    stats: &mut Stats,
361    count: num_bigint::BigUint,
362    peak_pairs: usize,
363    preprocessing: PreprocessReport,
364    compile_ms: u128,
365    diagram: Option<tididi::Tdd>,
366) -> Verdict {
367    if peak_pairs > 0 {
368        stats.peak_pairs = Some(peak_pairs as u64);
369    }
370    preprocessing.write(stats);
371    stats.compile_ms = Some(compile_ms as u64);
372    recorded_integer(stats, count, diagram)
373}
374
375/// What preprocessing did, in the four numbers the result record keeps. The
376/// modes carry them and read none of them: they travel from the frontend that
377/// measured them to the record.
378#[derive(Clone, Copy)]
379pub(crate) struct PreprocessReport {
380    pub(crate) preprocess_ms: u64,
381    pub(crate) backbone_ms: u64,
382    pub(crate) backbone_found: u64,
383    pub(crate) backbone_probes: u64,
384}
385
386impl PreprocessReport {
387    /// The report of one vitri run, its whole wall counted as preprocessing.
388    pub(crate) fn of(telemetry: &vitri::bundle::PreprocessTelemetry) -> Self {
389        PreprocessReport {
390            preprocess_ms: telemetry.total_ms,
391            backbone_ms: telemetry.backbone_ms.unwrap_or(0),
392            backbone_found: telemetry.backbone_found as u64,
393            backbone_probes: telemetry.backbone_probes as u64,
394        }
395    }
396
397    fn write(self, stats: &mut Stats) {
398        stats.preprocess_ms = Some(self.preprocess_ms);
399        stats.backbone_ms = Some(self.backbone_ms);
400        stats.backbone_found = Some(self.backbone_found);
401        stats.backbone_probes = Some(self.backbone_probes);
402    }
403}
404
405/// Which stage produced a model count, with the Arjun and cascade walls the
406/// record keeps beside it.
407#[derive(Clone, Copy)]
408pub(crate) struct Attribution {
409    /// Not an `Option`: every caller must name the stage, so a new answer
410    /// site cannot be added without saying which stage answers there.
411    pub(crate) answered_by: AnsweredBy,
412    /// `None` when Arjun did not run.
413    pub(crate) reduction: Option<Reduction>,
414    pub(crate) arjun_ms: Option<u128>,
415    pub(crate) cascade_ms: Option<u128>,
416}
417
418/// A model count, the cascade's or the streaming route's, with what the record
419/// keeps beside it. Nothing is materialized as a diagram, so there are no
420/// diagram sizes beyond the compile peak.
421pub(crate) struct McAnswer {
422    pub(crate) count: num_bigint::BigUint,
423    pub(crate) peak_pairs: usize,
424    pub(crate) preprocessing: PreprocessReport,
425    pub(crate) compile_ms: u128,
426    pub(crate) attribution: Attribution,
427}
428
429/// The verdict and the record of a model count.
430pub(crate) fn record_mc(stats: &mut Stats, config: &Config, answer: McAnswer) -> Verdict {
431    let McAnswer { count, peak_pairs, preprocessing, compile_ms, attribution } = answer;
432    stats.answered_by = Some(attribution.answered_by.token());
433    stats.reduction = attribution.reduction.map(Reduction::token);
434    stats.arjun_ms = attribution.arjun_ms.map(|ms| ms as u64);
435    stats.cascade_ms = attribution.cascade_ms.map(|ms| ms as u64);
436    record_vtree_candidate(stats, config);
437    record_integer(stats, count, peak_pairs, preprocessing, compile_ms, None)
438}
439
440/// The verdict and the record of a weighted count.
441///
442/// The count is `sign·10^(log10_abs)` in the bounded log domain, which every
443/// weighted count runs in (`src/stage/wmc.rs`), spelled in scientific notation
444/// ([`Count::Log`]), because the magnitude can be astronomically large or
445/// small and a plain f64 would overflow or underflow.
446pub(crate) fn record_wmc(
447    stats: &mut Stats,
448    config: &Config,
449    count: WeightedCount<tididi::diagram::WeightValue>,
450) -> Verdict {
451    use tididi::diagram::SignedLog;
452    record_vtree_candidate(stats, config);
453    let (satisfiable, s) = match count {
454        WeightedCount::Unsatisfiable => (false, SignedLog::zero()),
455        WeightedCount::Satisfiable(value) => {
456            (true, *value.as_log().expect("the weighted count runs in the log domain"))
457        }
458    };
459    recorded(stats, satisfiable, Count::Log(s), None)
460}
461
462/// A projected count, PMC's or PWMC's, with the two record fields its stages
463/// measure; [`crate::stage::run_stages`] fills it at the answer.
464pub(crate) struct Projected<V> {
465    pub(crate) value: V,
466    /// Variables in the show set of the problem the answering stage was
467    /// handed; `None` when there is no show set.
468    pub(crate) show_vars: Option<usize>,
469    /// The stages' wall, less the rewrites and the vtree construction.
470    pub(crate) compile_ms: u128,
471}
472
473impl<V> Projected<V> {
474    fn record(self, stats: &mut Stats) -> V {
475        stats.compile_ms = Some(self.compile_ms as u64);
476        stats.show_vars = self.show_vars.map(|n| n as u64);
477        self.value
478    }
479}
480
481/// The verdict and the record of a projected integer (PMC) count.
482pub(crate) fn record_pmc(stats: &mut Stats, answer: Projected<num_bigint::BigUint>) -> Verdict {
483    let count = answer.record(stats);
484    recorded_integer(stats, count, None)
485}
486
487/// The verdict and the record of a projected weighted (PWMC) count, an exact
488/// rational.
489pub(crate) fn record_pwmc(
490    stats: &mut Stats,
491    answer: Projected<WeightedCount<num_rational::BigRational>>,
492) -> Verdict {
493    use num_traits::Zero;
494    let (satisfiable, value) = match answer.record(stats) {
495        WeightedCount::Unsatisfiable => (false, num_rational::BigRational::zero()),
496        WeightedCount::Satisfiable(value) => (true, value),
497    };
498    recorded(stats, satisfiable, Count::Rational(value), None)
499}
500
501/// The plain compile's diagram, its count, and what the record keeps beside
502/// them.
503pub(crate) struct CompileAnswer {
504    pub(crate) tdd: tididi::Tdd,
505    /// The diagram's model count, over every original variable.
506    pub(crate) count: num_bigint::BigUint,
507    /// The largest diagram the answering compile held, in pairs.
508    pub(crate) peak_pairs: usize,
509    pub(crate) preprocessing: PreprocessReport,
510    pub(crate) compile_ms: u128,
511    pub(crate) answered_by: AnsweredBy,
512}
513
514/// The verdict and the record of the plain compile: the count, the diagram it
515/// was read off, which the answer carries, and the compile that answered.
516pub(crate) fn record_compile(stats: &mut Stats, answer: CompileAnswer) -> Verdict {
517    let CompileAnswer { tdd, count, peak_pairs, preprocessing, compile_ms, answered_by } = answer;
518    stats.answered_by = Some(answered_by.token());
519    stats.tdd_width = Some(tdd.max_width() as u64);
520    stats.tdd_pairs = Some(tdd.pair_count() as u64);
521    stats.tdd_nodes = Some(tdd.node_count() as u64);
522    record_integer(stats, count, peak_pairs, preprocessing, compile_ms, Some(tdd))
523}
524
525// ── The command line ─────────────────────────────────────────────────────────
526
527/// Print what the run answered as the command line asked, write the circuit
528/// under `--save-tdd` and the record under `--stats`, and return how the run
529/// ended.
530///
531/// Stdout, in order: the answer, or `s UNKNOWN` for a give-up under
532/// `--output-format mcc`; the `tdd written to` line; the record, when
533/// `--stats -`. A run that failed before opening its record writes none. An
534/// answer, a circuit or a record that cannot be written fails a run that
535/// otherwise succeeded, because the operator asked for it; a reader that
536/// closed stdout early is the exception ([`print`]).
537pub(crate) fn finish(solved: Result<Answer, Failure>, cli: &Cli) -> Result<(), Error> {
538    let mcc = cli.output_format == OutputFormat::Mcc;
539    let (outcome, stats) = match solved {
540        Ok(answer) => {
541            let delivered = print_answer(&answer, mcc).and_then(|()| match &cli.save_tdd_path {
542                Some(path) => {
543                    // The driver refuses `--save-tdd` on every mode but the
544                    // compile, and the compile always answers with its diagram.
545                    let tdd = answer.diagram.as_ref().expect("a compile answers with its diagram");
546                    save_diagram(tdd, path, mcc)
547                }
548                None => Ok(()),
549            });
550            let mut stats = answer.stats;
551            // An answer or a circuit that could not be written ends the run
552            // that computed it.
553            stats.exit_reason = Some(exit_reason(&delivered));
554            (delivered, Some(stats))
555        }
556        Err(Failure { error, stats }) => {
557            // The give-up is the run's outcome, whether or not its status
558            // line reaches stdout.
559            let _ = report_give_up(&error, mcc);
560            (Err(error), stats.map(|stats| *stats))
561        }
562    };
563    let written = match (&cli.stats_path, stats) {
564        (Some(path), Some(stats)) => write_record(&stats, path),
565        _ => Ok(()),
566    };
567    outcome.and(written)
568}
569
570/// Write the record as one JSON object at `path`, or on stdout for `-`.
571fn write_record(stats: &Stats, path: &Path) -> Result<(), Error> {
572    let json = serde_json::to_string(stats)
573        .map_err(|error| Error::Artifact(format!("could not build the stats record: {error}")))?;
574    if path.as_os_str() == "-" {
575        return print(&format!("{json}\n"));
576    }
577    std::fs::write(path, format!("{json}\n"))
578        .map_err(|error| Error::Artifact(format!("could not write {}: {error}", path.display())))
579}
580
581/// Write the compiled circuit to `path` and its vtree to `<path>.vtree`, and
582/// say so on stdout under the plain output format.
583fn save_diagram(tdd: &tididi::diagram::Tdd, path: &Path, mcc: bool) -> Result<(), Error> {
584    // The `.tdd` format labels vtree nodes but does not describe the tree,
585    // and `tididi::io::load_tdd` takes the vtree as an argument, so the
586    // vtree goes beside the circuit in its own text format.
587    let mut vtree_path = path.as_os_str().to_owned();
588    vtree_path.push(".vtree");
589    let vtree_path = PathBuf::from(vtree_path);
590    let (shown, vtree_shown) = (path.display(), vtree_path.display());
591    tididi::io::save_tdd(tdd, path)
592        .map_err(|error| Error::Artifact(format!("could not write the diagram to {shown}: {error}")))?;
593    std::fs::write(&vtree_path, tdd.vtree().to_text())
594        .map_err(|error| Error::Artifact(format!("could not write the vtree to {vtree_shown}: {error}")))?;
595    if mcc {
596        return Ok(());
597    }
598    print(&format!(
599        "tdd written to {shown}, vtree to {vtree_shown}: {} nodes, {} pairs\n",
600        tdd.node_count(),
601        tdd.pair_count(),
602    ))
603}
604
605/// Write `text` to stdout, the one writer of everything the command line
606/// prints there. A reader that stops early, such as `head` or a pager that
607/// quit, closes the pipe: that is not an error, and the run goes on without
608/// stdout. Any other failed write is [`Error::Artifact`].
609pub(crate) fn print(text: &str) -> Result<(), Error> {
610    use std::io::Write as _;
611    let mut stdout = std::io::stdout().lock();
612    match stdout.write_all(text.as_bytes()).and_then(|()| stdout.flush()) {
613        Err(error) if error.kind() != std::io::ErrorKind::BrokenPipe => {
614            Err(Error::Artifact(format!("could not write to stdout: {error}")))
615        }
616        _ => Ok(()),
617    }
618}
619
620/// log10 of a non-negative big integer to ~double precision, for the MCC
621/// `c s log10-estimate` line. Returns `-inf` for zero, exactly as the
622/// MC-Competition output spec (v1.2 §3) requires when the count is 0.
623fn log10_biguint(n: &num_bigint::BigUint) -> f64 {
624    if *n == num_bigint::BigUint::ZERO {
625        return f64::NEG_INFINITY;
626    }
627    // Keep the top ~53 bits (one f64 mantissa) and account for the rest via
628    // the binary exponent: log10(n) = log10(n >> s) + s*log10(2). This stays
629    // accurate for counts far larger than f64's range (2^1024).
630    let bits = n.bits(); // index of the MSB + 1, >= 1
631    let shift = (bits as usize).saturating_sub(53);
632    let high = (n >> shift).to_u64_digits().first().copied().unwrap_or(1) as f64;
633    high.log10() + (shift as f64) * std::f64::consts::LOG10_2
634}
635
636/// The competition's status line. "The solver may output `UNKNOWN`, but is not
637/// allowed to output any other value than these three": `None` is a run that
638/// gave up.
639fn status_line(satisfiable: Option<bool>) -> &'static str {
640    match satisfiable {
641        Some(true) => "s SATISFIABLE",
642        Some(false) => "s UNSATISFIABLE",
643        None => "s UNKNOWN",
644    }
645}
646
647/// The MCC answer to a run that gave up: `s UNKNOWN` under
648/// `--output-format mcc`, the one status line the format allows without a
649/// count.
650///
651/// Every task's give-up reaches the driver as [`Error::Incomplete`], after the
652/// stage that gave up said why on stderr, so [`finish`] prints this once
653/// rather than each stage printing it. A run stopped by any other error has
654/// refused the instance or failed, and prints nothing on stdout.
655fn report_give_up(error: &Error, mcc: bool) -> Result<(), Error> {
656    if matches!(error, Error::Incomplete(_)) && mcc {
657        return print(&format!("{}\n", status_line(None)));
658    }
659    Ok(())
660}
661
662/// Print one answer.
663///
664/// Under `--output-format mcc` it is the four lines of the Model Counting
665/// Competition's output format (data format v1.2 §3), in the format's order,
666/// for every task:
667///
668/// ```text
669/// s SATISFIABLE | s UNSATISFIABLE
670/// c s type mc | wmc | pmc | pwmc
671/// c s log10-estimate <log10 of the count> | c s neglog10-estimate <log10 of -count>
672/// c s <solver type> <precision> <notation> <count>
673/// ```
674///
675/// and otherwise one plainly worded line. This is the only printer of either,
676/// so no task can drift from the order or the spelling. The status line is the
677/// solver's verdict, not the count's: a zero weight, or weights that cancel,
678/// make the weighted count of a satisfiable formula zero.
679fn print_answer(answer: &Answer, mcc: bool) -> Result<(), Error> {
680    let count = &answer.count;
681    if !mcc {
682        return print(&format!("{}: {count}\n", plain_label(answer.mode)));
683    }
684    let (log10, negative) = magnitude(count);
685    let estimate = if negative { "neglog10-estimate" } else { "log10-estimate" };
686    print(&format!(
687        "{}\nc s type {}\nc s {estimate} {log10}\nc s {}\n",
688        status_line(Some(answer.satisfiable)),
689        task(answer.mode),
690        exact(count),
691    ))
692}
693
694/// The task as `c s type` names it. The plain compile's count is a model count
695/// too, read off the diagram over every original variable, so it answers as
696/// `mc`.
697fn task(mode: Mode) -> &'static str {
698    match mode {
699        Mode::Compile => Mode::Mc.token(),
700        mode => mode.token(),
701    }
702}
703
704/// The words before the count on the plain answer line.
705fn plain_label(mode: Mode) -> &'static str {
706    match mode {
707        Mode::Compile | Mode::Mc => "model count",
708        Mode::Wmc => "weighted count",
709        Mode::Pmc => "projected model count",
710        Mode::Pwmc => "projected weighted count",
711    }
712}
713
714/// log10 of the count's magnitude, `-inf` for a zero count, printed in full
715/// double precision; and whether the count is below zero, which only negative
716/// weights make it.
717fn magnitude(count: &Count) -> (f64, bool) {
718    match count {
719        Count::Integer(n) => (log10_biguint(n), false),
720        Count::Rational(r) => (
721            log10_biguint(r.numer().magnitude()) - log10_biguint(r.denom().magnitude()),
722            r.numer().sign() == num_bigint::Sign::Minus,
723        ),
724        Count::Log(s) => (s.log10_abs(), s.sign < 0),
725    }
726}
727
728/// The last MCC answer line after its `c s `: solver type, precision, notation
729/// and the count, in each task's spelling.
730fn exact(count: &Count) -> String {
731    match count {
732        Count::Integer(n) => format!("exact arb int {n}"),
733        // The standard MCC weighted-float answer: "exact" is the solver type,
734        // and `double` says the value is bounded-precision f64.
735        Count::Log(_) => format!("exact double prec-sci {count}"),
736        Count::Rational(_) => format!("exact arb frac {count}"),
737    }
738}
739
740#[cfg(test)]
741#[path = "tests/output.rs"]
742mod tests;