tididi_cnf/compile/mod.rs
1//! The compiler: a CNF formula and a vtree in, a Tree Decision Diagram out.
2//!
3//! [`compile_cnf`] groups the clauses by their scope, the lowest vtree node
4//! whose subtree covers every variable of the clause, and works bottom-up: at
5//! each vtree node it conjoins the children's diagrams, then the clauses scoped
6//! there, a batch at a time, minimizing as it goes. A formula whose clauses
7//! fall into independent components, groups that share no variable, is
8//! compiled one component at a time; [`ComponentPlan`] says whether the
9//! compiler finds them, takes them from the vtree construction, or compiles
10//! the formula whole. [`Output`] says what the compile produces: the Boolean
11//! diagram, or a counting diagram that marginalizes each level once its last
12//! clause is in, weighted or not, with variables forgotten for a projected
13//! count, or the Boolean diagram of such a projection.
14//!
15//! A finished compile returns [`Compiled`]: the diagram, the largest size it
16//! reached, and how many projected variables it left free. It never returns
17//! a partial diagram: a compile that cannot finish says why with [`GiveUp`].
18//!
19//! The run [`count`](crate::count) counts with this same compile, one
20//! component at a time where the formula splits, and retries a count that ran
21//! out of memory on smaller pieces of the formula.
22
23mod batch;
24mod cofactor;
25mod component;
26mod marginalize;
27mod minimize;
28pub(crate) mod sat_prune;
29mod schedule;
30mod scope;
31mod stall;
32mod step;
33
34use std::cell::Cell;
35use std::collections::HashSet;
36use std::sync::Arc;
37use std::time::Instant;
38
39use num_bigint::BigUint;
40use tididi::{Engine, OperationError};
41use tididi::diagram::{Arithmetic, RationalWeights, Tdd, WeightStore, WeightValue};
42use tididi::vtree::{VarId, Vtree};
43use vitri::cnf::CnfFormula;
44
45use crate::Error;
46use crate::budget::{deadline_passed, deadline_scope};
47use crate::problem::WeightedCount;
48use crate::vtree::{Component, ComponentVtree};
49use component::{CompileJob, PassOutput, compile_split, detected_split};
50pub(crate) use component::{check_partition, check_vtree};
51use minimize::minimize_full;
52use step::compile_component;
53
54pub(crate) use stall::{StallRule, StallWatch};
55
56/// Which independent components a compile splits the formula into.
57#[derive(Clone, Copy, Default)]
58pub enum ComponentPlan<'a> {
59 /// Find them; with two or more, compile each on a balanced vtree of its
60 /// own and leave `vtree` unused.
61 #[default]
62 Detect,
63 /// Compile the formula whole, against the supplied root vtree.
64 Whole,
65 /// Compile the components the caller already built trees for, in this order.
66 Split(&'a [ComponentVtree]),
67}
68
69impl<'a> ComponentPlan<'a> {
70 /// Interpret a construction's component trees: a component list is an
71 /// exact split plan, and absence is an explicit whole-formula plan.
72 pub fn from_components(components: Option<&'a [ComponentVtree]>) -> Self {
73 match components {
74 Some(components) => Self::Split(components),
75 None => Self::Whole,
76 }
77 }
78}
79
80/// What a run sets for one compile, beyond what the compile is asked to
81/// produce: the run's SAT pruning, whether the compile is a Shannon
82/// cofactor's, and the per-step wall its internal steps run under.
83#[derive(Clone, Copy, Default)]
84pub(crate) struct CompileRun<'a> {
85 /// The run's SAT pruning, which a compile that runs to its end adds its
86 /// tallies to; `None` prunes nothing.
87 pub(crate) sat_prune: Option<&'a sat_prune::SatPrune>,
88 /// A recovery cofactor's compile, which skips the content-twin scan: the
89 /// cascade recompiles cofactors under memory pressure, and scanning a
90 /// diagram about to be split or dropped adds no useful work.
91 pub(crate) cofactor: bool,
92 /// The per-step wall the compile's internal steps run under, and where it
93 /// reports a step that stopped on it ([`StallWatch`]); `None` for no
94 /// per-step wall. Only the model count's primary compile has one.
95 pub(crate) stall_watch: Option<&'a StallWatch>,
96}
97
98/// Why a compile has no diagram to return.
99#[derive(Clone, PartialEq, Eq, Debug)]
100pub enum GiveUp {
101 /// The compile stopped short at its deadline. A caller with a fallback
102 /// runs it; nothing partial is ever returned, so a count taken from a
103 /// compile that did not give up is exact. The library's
104 /// `OperationError::Stopped` is the same stop, one level down.
105 Stopped,
106 /// The compile stopped short because an apply was refused under the batch
107 /// limits: `limit` is `OverBudget` (memory) or `OutputCap` (size), and
108 /// `refused_bytes` the request the allocator refused, when it was one. A
109 /// caller treats it as [`GiveUp::Stopped`].
110 Refused {
111 /// The limit the apply reached.
112 limit: OperationError,
113 /// The size of the refused allocation, if the allocator refused one.
114 refused_bytes: Option<u64>,
115 },
116 /// An allocation was refused while reducing the finished diagram. The
117 /// binary reports it with the diagnostic [`crate::Error`] carries, which is
118 /// what a benchmark harness reads to classify the run as out of memory.
119 OutOfMemory,
120 /// The caller's input does not fit together: the vtree does not place
121 /// exactly the formula's variables, a [`ComponentPlan::Split`] does not
122 /// partition the formula, or the output's variables or weights do not
123 /// cover the requested formula. The text says which.
124 Invalid(String),
125}
126
127impl GiveUp {
128 /// The one reading of an `OperationError` a compile makes as it gives up:
129 /// a refused apply names its limit and, when the allocator refused it,
130 /// the size, which separates a request no machine could satisfy from a
131 /// machine that is full. `Stopped` is the compile's own wall, or an inner
132 /// budget's expiry riding the same unwind.
133 ///
134 /// # Panics
135 ///
136 /// On any other error: invalid operands are a compiler bug, not a stop.
137 pub(crate) fn from_operation(eng: &Engine, e: OperationError) -> Self {
138 match e {
139 OperationError::OverBudget | OperationError::OutputCap => {
140 GiveUp::Refused { limit: e, refused_bytes: eng.limits().meters().refused_reserve_bytes }
141 }
142 OperationError::Stopped => GiveUp::Stopped,
143 other => panic!("compiler produced invalid operands: {other}"),
144 }
145 }
146
147 /// Say on stderr why the compile stopped, for a refused apply: what a
148 /// benchmark log is read for to tell a memory stop from a deadline. The
149 /// other give-ups print nothing; their callers say what they do next.
150 pub(crate) fn report(&self) {
151 if let GiveUp::Refused { .. } = self {
152 eprintln!("[compile] stopped: {self}");
153 }
154 }
155}
156
157/// A compile whose inputs the crate built, as its caller reads it: a give-up
158/// is reported and becomes `None`, for the caller's fallback.
159///
160/// # Panics
161///
162/// On [`GiveUp::Invalid`]: the crate built inputs that do not fit together.
163pub(crate) fn settle<T>(result: Result<T, GiveUp>) -> Option<T> {
164 match result {
165 Ok(value) => Some(value),
166 Err(GiveUp::Invalid(why)) => panic!("the crate built a compile input that does not fit: {why}"),
167 Err(give_up) => {
168 give_up.report();
169 None
170 }
171 }
172}
173
174impl std::fmt::Display for GiveUp {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 GiveUp::Stopped => f.write_str("the compile stopped at its deadline"),
178 GiveUp::Refused { limit, refused_bytes: Some(b) } => write!(
179 f,
180 "the allocator refused {b} bytes ({:.1} GiB) during apply (cap={limit:?})",
181 *b as f64 / (1u64 << 30) as f64,
182 ),
183 GiveUp::Refused { limit, refused_bytes: None } => {
184 write!(f, "apply reached a limit with no recorded allocator refusal (cap={limit:?})")
185 }
186 GiveUp::OutOfMemory => f.write_str("an allocation was refused while reducing the compiled diagram"),
187 GiveUp::Invalid(why) => f.write_str(why),
188 }
189 }
190}
191
192impl std::error::Error for GiveUp {}
193
194/// A give-up as the run reports it: a stop or a refusal ends the run without
195/// a count, and inputs that do not fit fail the compile stage.
196impl From<GiveUp> for Error {
197 fn from(give_up: GiveUp) -> Self {
198 match give_up {
199 GiveUp::Stopped | GiveUp::Refused { .. } => Error::compile_gave_up(),
200 GiveUp::OutOfMemory => Error::out_of_memory(),
201 GiveUp::Invalid(why) => Error::Stage(format!("compile failed: {why}")),
202 }
203 }
204}
205
206/// The heap around one [`release_failed_compile`], and how its purge went:
207/// `Err` carries the mallctl failure.
208pub(crate) struct Released {
209 pub(crate) before: crate::mem::HeapStats,
210 pub(crate) after: crate::mem::HeapStats,
211 pub(crate) purged: Result<(), libc::c_int>,
212}
213
214impl Released {
215 /// The address space the allocator held, `mapped + retained`, before and
216 /// after the purge, in whole MiB: the pair every purge report prints.
217 pub(crate) fn held_mib(&self) -> (u64, u64) {
218 let held = |heap: &crate::mem::HeapStats| (heap.mapped + heap.retained) / crate::MIB;
219 (held(&self.before), held(&self.after))
220 }
221}
222
223/// Give back what a failed compile left behind, before the next compile
224/// starts on the same engine.
225///
226/// The failed compile's diagrams are already dropped. What outlives them is
227/// the engine's retained apply scratch (`Engine::clear_scratch`: the sparse
228/// workspace and level pools the failure left pinned at full capacity), freed
229/// first so the purge can advise its pages away, and the allocator's dirty
230/// pages, which the purge ([`crate::mem::purge_all_arenas`]) returns. The
231/// decay setting is left to the pressure check in `crate::mem`, so the
232/// compiles that follow keep their freed pages for reuse until the footprint
233/// nears the address-space cap. Every compile that follows a failed one on
234/// the same engine runs this first: the model count's fallbacks, its stall
235/// recompile and Shannon split, and the plain compile's retry on a second
236/// vtree.
237pub(crate) fn release_failed_compile(eng: &Engine) -> Released {
238 let before = crate::mem::HeapStats::read();
239 eng.clear_scratch();
240 let purged = crate::mem::purge_all_arenas();
241 Released { before, after: crate::mem::HeapStats::read(), purged }
242}
243
244/// What a compile produces.
245#[derive(Clone, Default)]
246pub enum Output<'a> {
247 /// The formula's Boolean diagram.
248 #[default]
249 Function,
250 /// A counting diagram: each level is marginalized as soon as the last
251 /// clause mentioning its variables lands, so the diagram's `model_count` is
252 /// the formula's and memory is freed as the compile climbs.
253 Count,
254 /// [`Output::Count`] with literal weights: the marginalization carries
255 /// values in `arithmetic` instead of integers, and the result holds the
256 /// store [`tididi::Engine::weighted_value`] reads the weighted count from.
257 WeightedCount {
258 /// The weight of each literal, covering every formula variable.
259 weights: RationalWeights,
260 /// The domain the values are computed in.
261 arithmetic: Arithmetic,
262 },
263 /// [`Output::Count`] of the formula with these variables ∃-forgotten during
264 /// the compile, each at its completion node, so the diagram never
265 /// materializes the projected cone. A forgotten variable is left free and
266 /// counts ×2, so a caller divides the count by `2^`[`Compiled::forgotten`].
267 /// Every leaf level stays Boolean, so a later forget can read its label.
268 ProjectedCount(&'a [VarId]),
269 /// The Boolean diagram of the formula with these variables ∃-forgotten
270 /// during the compile, as [`Output::ProjectedCount`] forgets them, each
271 /// left free. No level is marginalized, so a weighted fold can read the
272 /// whole result.
273 ProjectedFunction(&'a [VarId]),
274}
275
276/// What a whole-formula compile is being asked for.
277///
278/// [`CompileOptions::default`] is the plain Boolean compile with component detection —
279/// `compile_cnf(eng, &formula, &vtree, CompileOptions::default())`.
280#[derive(Default)]
281pub struct CompileOptions<'a> {
282 /// Which components to compile separately, and against which vtrees.
283 pub plan: ComponentPlan<'a>,
284 /// What the compile produces. Every variable it lists must belong to the
285 /// formula (`1..=formula.num_vars()`).
286 pub output: Output<'a>,
287 /// The wall-clock the compile gives up at. When set it is also installed
288 /// as the apply-level cut for this compile, so one giant un-yielding
289 /// conjoin can be stopped mid-apply rather than only at a step boundary.
290 /// `None` leaves any enclosing apply deadline in force.
291 pub deadline: Option<Instant>,
292}
293
294/// What a finished compile returns.
295pub struct Compiled {
296 /// The compiled diagram, on the vtree described at [`compile_cnf`].
297 pub diagram: Tdd,
298 /// How many of the variables a projected [`Output`] forgets are free in
299 /// `diagram`, each doubling its count: those ∃-forgotten, and those in no
300 /// clause, which a component split grafts in free or a clause-free formula
301 /// leaves unconstrained. Zero for a compile that forgets nothing.
302 pub forgotten: usize,
303 /// The largest diagram, in pairs, any step of the compile reached; the
304 /// largest over the components of a split compile, which run one after
305 /// another.
306 pub peak_pairs: usize,
307}
308
309impl Compiled {
310 /// The count `diagram` holds onto the variables a projected [`Output`]
311 /// keeps: its model count with the doubling of each
312 /// [`forgotten`](Self::forgotten) variable divided out. The plain model
313 /// count for a compile that forgets nothing.
314 pub(crate) fn projected_count(&self, eng: &Engine) -> Result<BigUint, OperationError> {
315 Ok(eng.model_count(&self.diagram)? >> self.forgotten)
316 }
317}
318
319/// Compile a CNF formula into a canonical TDD, guided by the vtree.
320///
321/// By default ([`ComponentPlan::Detect`]) a formula whose clauses fall into
322/// independent components, groups that share no variable, is compiled one
323/// component at a time, each on a balanced vtree of its own, and the
324/// components' diagrams are grafted into one; a formula with one component
325/// is compiled on `vtree`. [`CompileOptions::plan`] can instead ask for the
326/// whole formula on `vtree`, or for the caller's own component vtrees.
327///
328/// The result's vtree, the one its diagram carries, is the vtree to build any
329/// further operand on: [`and`](tididi::and) and the other operations refuse
330/// operands on different vtrees. It is `vtree` itself only when the formula was
331/// compiled whole and no rotation fired. A split compile's result is on a vtree
332/// grafted from the components' vtrees, and a counting compile may rotate its
333/// vtree to bring marginal levels together, onto a copy; `vtree` itself is
334/// never changed.
335///
336/// An [`Err`] is a give-up, never a partial diagram, so any count taken from an
337/// `Ok` is exact. A caller that runs this under a deadline it set — an attempt
338/// self-limiting at the wall, a reactive single shot with a fallback — treats
339/// [`GiveUp::Stopped`] as its own loss.
340///
341/// An empty clause makes the result the false diagram, on `vtree`.
342///
343/// # Errors
344///
345/// - [`GiveUp::Stopped`]: [`CompileOptions::deadline`] passed.
346/// - [`GiveUp::Refused`]: an apply was refused under the limits of the batch
347/// `eng` belongs to.
348/// - [`GiveUp::OutOfMemory`]: an allocation was refused while reducing the
349/// finished diagram.
350/// - [`GiveUp::Invalid`]: `vtree`, or a component's vtree, does not place
351/// exactly its formula's variables, or a [`ComponentPlan::Split`] does not
352/// partition the formula; a projected variable is outside the formula, or
353/// a literal weight is missing.
354pub fn compile_cnf(
355 eng: &Engine,
356 formula: &CnfFormula,
357 vtree: &Arc<Vtree>,
358 opts: CompileOptions<'_>,
359) -> Result<Compiled, GiveUp> {
360 let sat_prune = sat_prune::SatPrune::default();
361 compile_with(eng, formula, vtree, opts, CompileRun { sat_prune: Some(&sat_prune), ..CompileRun::default() })
362}
363
364/// [`compile_cnf`] under what the run sets for it (`run`).
365pub(crate) fn compile_with(
366 eng: &Engine,
367 formula: &CnfFormula,
368 vtree: &Arc<Vtree>,
369 opts: CompileOptions<'_>,
370 run: CompileRun<'_>,
371) -> Result<Compiled, GiveUp> {
372 let CompileOptions { plan, output, deadline } = opts;
373 // The apply-level deadline, whose Drop restores the previous value on every
374 // return and panic path.
375 let _apply_deadline = deadline_scope(eng, deadline);
376
377 component::check_vtree(formula.num_vars(), vtree)?;
378 if let ComponentPlan::Split(components) = plan {
379 component::check_partition(formula, components)?;
380 }
381
382 let weights;
383 let forget: HashSet<VarId>;
384 let output = match output {
385 Output::Function => PassOutput::Function,
386 Output::Count => PassOutput::Count(None),
387 Output::WeightedCount { weights: algebra, arithmetic } => {
388 // Literal tables cover a contiguous range starting at one. Ask
389 // the library to validate its last required entry before either
390 // a constant shortcut or a component can read the table.
391 if formula.num_vars() > 0 {
392 algebra.restated(&[VarId(formula.num_vars())])
393 .map_err(|error| GiveUp::Invalid(error.to_string()))?;
394 }
395 weights = WeightStore::new(algebra, arithmetic);
396 PassOutput::Count(Some(&weights))
397 }
398 Output::ProjectedCount(vars) => {
399 forget = formula_vars(formula, vars)?;
400 PassOutput::ProjectedCount(&forget)
401 }
402 Output::ProjectedFunction(vars) => {
403 forget = formula_vars(formula, vars)?;
404 PassOutput::ProjectedFunction(&forget)
405 }
406 };
407 let prune_spent_us = Cell::new(0);
408 let job = CompileJob { deadline, output, run, prune_spent_us: &prune_spent_us };
409 eng.limits().poll_host_work(0).map_err(|error| GiveUp::from_operation(eng, error))?;
410
411 if formula.clauses().is_empty() {
412 let diagram = output.with_store(eng.one(vtree));
413 // Every variable is free, ×2 in the raw count, a projected one included.
414 let forgotten = output.forget().map_or(0, |forget| {
415 (1..=vtree.num_vars()).filter(|&v| forget.contains(&VarId(v))).count()
416 });
417 return Ok(Compiled { diagram, forgotten, peak_pairs: 0 });
418 }
419 // The empty clause is the constant false, which the whole-formula pass
420 // answers at once: there is nothing to split into components.
421 let plan = if formula.clauses().iter().any(|c| c.literals.is_empty()) { ComponentPlan::Whole } else { plan };
422
423 // Two compilation paths:
424 // 1. A split — the caller's per-component vtrees, or detected components
425 // on balanced ones: compile each component on its own vtree (a
426 // marginal-cluster rotation stays confined to that vtree Arc), then
427 // *graft* the per-component TDDs into a single TDD on a freshly built
428 // grafted vtree.
429 // 2. The whole formula (asked for, or a detected single component):
430 // compile on the caller's supplied root and minimize.
431 match plan {
432 ComponentPlan::Split(component_vtrees) => {
433 return compile_split(eng, formula, component_vtrees, job);
434 }
435 ComponentPlan::Detect => {
436 if let Some(detected) = formula.detect_components() {
437 return compile_split(eng, formula, &detected_split(eng, formula, detected), job);
438 }
439 }
440 ComponentPlan::Whole => {}
441 }
442
443 // Whole-formula compile.
444 let mut result = compile_component(eng, formula, vtree, job)?;
445 minimize_full(eng, &mut result.diagram, job.run.cofactor).map_err(|_| GiveUp::OutOfMemory)?;
446 Ok(result)
447}
448
449/// The projected variables as a set, refusing ids outside the formula.
450fn formula_vars(formula: &CnfFormula, vars: &[VarId]) -> Result<HashSet<VarId>, GiveUp> {
451 vars.iter().map(|&var| {
452 if var.0 == 0 || var.0 > formula.num_vars() {
453 Err(GiveUp::Invalid(format!(
454 "projected variable {} is outside the formula's 1..={} variables", var.0, formula.num_vars(),
455 )))
456 } else {
457 Ok(var)
458 }
459 }).collect()
460}
461
462/// The weighted count of `formula` under `algebra`, computed in
463/// `arithmetic`: the [`Output::WeightedCount`] compile, read at its root. It
464/// marginalizes as it climbs, so it frees memory as the model count does.
465///
466/// The verdict is the diagram's: a weighted compile stores no unsatisfiable
467/// node, so the formula is unsatisfiable exactly when the result is the false
468/// constant, whatever its weights.
469///
470/// # Returns
471///
472/// `None` when the compile gives up, at `deadline` or refused under the batch
473/// limits (a refusal is reported on stderr), when the readout is refused, or
474/// when the result has no weighted value.
475pub(crate) fn weighted_count(
476 eng: &Engine,
477 formula: &CnfFormula,
478 vtree: &Arc<Vtree>,
479 components: ComponentPlan<'_>,
480 algebra: RationalWeights,
481 arithmetic: Arithmetic,
482 deadline: Option<Instant>,
483) -> Option<WeightedCount<WeightValue>> {
484 let tdd = settle(compile_cnf(
485 eng,
486 formula,
487 vtree,
488 CompileOptions {
489 plan: components,
490 output: Output::WeightedCount { weights: algebra, arithmetic },
491 deadline,
492 },
493 ))?
494 .diagram;
495 if tdd.is_zero() {
496 return Some(WeightedCount::Unsatisfiable);
497 }
498 eng.weighted_value(&tdd).ok().flatten().map(WeightedCount::Satisfiable)
499}
500
501/// The model count of `formula`, one of `components` at a time: each
502/// component's clauses are renumbered, `build_sub_vtree` builds its vtree,
503/// and its count multiplies into the product before its diagram is dropped,
504/// so the memory the count needs is the largest component's, not the sum.
505/// Each variable in no component doubles the product.
506///
507/// `forget` makes it the count [`Output::ProjectedCount`] gives, onto the
508/// other variables: each component's count is divided by its own forgotten
509/// variables before it enters the product, so the result needs no correction.
510///
511/// `deadline` is installed as the apply-level cut, as [`compile_cnf`] does. It
512/// does not bound `build_sub_vtree`, which runs no library operation. `run`
513/// is what the run sets for each component's compile.
514///
515/// # Returns
516///
517/// The count and the largest diagram size any component's compile reached, in
518/// pairs; or `None` when `build_sub_vtree` built no vtree for a component, or
519/// a component's compile gave up, at `deadline` or refused under the batch
520/// limits (a refusal is reported on stderr).
521pub(crate) fn model_count_streaming<F>(
522 eng: &Engine,
523 formula: &CnfFormula,
524 components: &[Component],
525 mut build_sub_vtree: F,
526 forget: Option<&[VarId]>,
527 deadline: Option<Instant>,
528 run: CompileRun<'_>,
529) -> Option<(BigUint, usize)>
530where
531 F: FnMut(&CnfFormula) -> Option<Arc<Vtree>>,
532{
533 let forget = settle(forget.map(|vars| formula_vars(formula, vars)).transpose())?;
534
535 if formula.clauses().is_empty() {
536 return Some((
537 BigUint::from(1u32) << component::streaming_free_var_count(formula, &[], forget.as_ref()),
538 0,
539 ));
540 }
541
542 let scratch = &mut component::RemapScratch::default();
543 // Restored when the count returns, on every path.
544 let _apply_deadline_guard = deadline_scope(eng, deadline);
545
546 let prune_spent_us = Cell::new(0);
547 let mut total = BigUint::from(1u32);
548 let mut peak = 0usize;
549 for comp in components {
550 if deadline_passed(deadline) {
551 return None;
552 }
553 component::remap_component_clauses_into(
554 scratch,
555 formula,
556 &comp.clause_indices,
557 &comp.local_to_global,
558 );
559 let sub_formula = scratch.sub();
560 let sub_vtree = build_sub_vtree(sub_formula)?;
561 settle(component::check_vtree(sub_formula.num_vars(), &sub_vtree))?;
562 // The projected variables, in this component's local numbering.
563 let comp_forget =
564 forget.as_ref().map(|forget| component::local_vars_in(&comp.local_to_global, forget));
565 let (sub_count, sub_peak) = {
566 let output = comp_forget.as_ref().map_or(PassOutput::Count(None), PassOutput::ProjectedCount);
567 let job = CompileJob { deadline, output, run, prune_spent_us: &prune_spent_us };
568 let compiled = settle(compile_component(eng, sub_formula, &sub_vtree, job))?;
569 (compiled.projected_count(eng).ok()?, compiled.peak_pairs)
570 };
571 peak = peak.max(sub_peak);
572 // A zero component zeroes the product: skip the rest of the loop.
573 if sub_count == BigUint::ZERO {
574 return Some((sub_count, peak));
575 }
576 total *= sub_count;
577 }
578 total <<= component::streaming_free_var_count(formula, components, forget.as_ref());
579 Some((total, peak))
580}
581
582#[cfg(test)]
583mod tests;