Skip to main content

logicaffeine_proof/
fraig.rs

1//! Combinational equivalence by incremental SAT sweeping, certified by one derivation.
2//!
3//! [`crate::sweep`] puts each candidate to a fresh solver over a formula of its own and certifies each lemma by an LRAT
4//! refutation of that formula. Here every question is a solve under assumptions on one solver, so what one query
5//! learns later ones keep (Mishchenko, Chatterjee, Brayton & Een, *Improvements to combinational equivalence
6//! checking*, ICCAD 2006, the scheme of ABC's `&cec`). The solver holds the clauses of the miter's formula ([`formula`]:
7//! the Tseitin clauses of every And node, node `k` the variable `k`) only for the cones its queries have asked about,
8//! encoded as each query needs them, and decides only on those nodes, so a query's search stays in its cone. Its
9//! variables are numbered as it meets them, so a solver is as large as what it was asked, and once it has
10//! [`Options::recycle`] of them a fresh one replaces it. It holds the reduced graph, not the miter:
11//! each node defined over its fan-ins' representatives (`n ↔ rep(x) ∧ rep(y)`, logged the first time a solver needs
12//! it), so the logic two sides share once merged is one variable to it and no chain of equalities.
13//!
14//! The sweep runs in node order, each node with a representative literal. A node whose fan-ins' representatives make
15//! a constant, a copy of one of them, or the pair an earlier node has, is that constant, copy or node, with no solver
16//! call (structural hashing over representatives). Any other node is put against the earlier node whose simulation
17//! signature it shares: `n ∧ ¬r`, then `¬n ∧ r`, under assumptions, within a conflict budget; a model is a
18//! counterexample, added to the simulation to split the class. The budget starts small, since a query that runs out
19//! is mostly one the merges around it would make easy: when a round leaves an output pair open, the next sweeps afresh
20//! at ten times the budget ([`Options::rounds`]).
21//!
22//! Every clause made goes into one log with its LRAT hints, in the order made, each hint the id of a clause of the
23//! formula or of the log before it: a clause a solver learns with the chain its conflict analysis resolved, a solved
24//! merge `(¬n ∨ r)`, `(n ∨ ¬r)` with the chain of the refutation of its assumptions, and a structural merge or a reduced
25//! definition with the chain unit propagation finds through the definitions and merges it rests on. A solver's learned
26//! clauses that no clause made in its time rests on go with it when it is replaced, since no later clause can name
27//! them. An output pair proven equal adds its own two clauses, the claims, and the derivation is the steps the claims
28//! reach back through their hints, in order. [`Report::check`] rebuilds the formula from the miter and replays it
29//! ([`crate::lrat::check_derivation`]). A difference is a model whose inputs, evaluated, separate the pair.
30
31use std::collections::{HashMap, HashSet};
32
33use crate::aig::{Aig, AigLit, Node};
34use crate::cdcl::{BudgetedResult, Lit, Solver};
35use crate::lrat::{self, Lrat};
36use crate::sweep::{miter, Miter, MiterError, Verdict};
37
38/// How [`decide`] searches.
39#[derive(Clone, Copy, Debug)]
40pub struct Options {
41    /// Random simulation words (64 patterns each) the first signatures are drawn from.
42    pub sim_words: usize,
43    pub seed: u64,
44    /// Conflicts a sweeping query of the first round may spend; 0 is unlimited.
45    pub budget: u64,
46    /// Conflicts an output pair's query of the last round may spend; 0 is unlimited.
47    pub final_budget: u64,
48    /// The most rounds a decision takes. Each sweeps afresh at ten times the budget of the one before and puts every
49    /// output pair to the solver within a hundred times its budget; the first round that leaves no pair open is the
50    /// last, and the last round's pairs are put within `final_budget`. A small first budget wastes no conflict on
51    /// queries the merges around them make easy later, and a round left short is no worse than one at its budget.
52    pub rounds: usize,
53    /// How many classes a node is put against before it stays apart.
54    pub attempts: usize,
55    /// The variables a solver holds before a fresh one replaces it.
56    pub recycle: usize,
57    /// How many clauses a solver's part of the log holds before what nothing a later clause can name is let go, and
58    /// beyond twice what each collection leaves.
59    pub collect: usize,
60    /// What to report the search's cost to as it goes, if anything.
61    pub progress: Option<Progress>,
62}
63
64impl Default for Options {
65    fn default() -> Options {
66        Options {
67            sim_words: 16,
68            seed: 0x5eed_c0de_f00d_a16e,
69            budget: 30,
70            final_budget: 0,
71            attempts: 4,
72            rounds: 3,
73            recycle: 100_000,
74            collect: 100_000,
75            progress: None,
76        }
77    }
78}
79
80/// A report of a long search: `report` is handed the cost so far once every `every` nodes swept, once the sweep is done,
81/// and once the output pairs are, before the derivation is taken from the log.
82#[derive(Clone, Copy, Debug)]
83pub struct Progress {
84    pub every: usize,
85    pub report: fn(&Stats),
86}
87
88/// What a decision cost.
89#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct Stats {
91    /// The miter's nodes, those the pairs reach (all the sweep passes: a node no pair reads cannot help decide one),
92    /// and how many of those the sweep has passed.
93    pub nodes: usize,
94    pub cone: usize,
95    pub swept: usize,
96    /// The rounds the decision took; the rest of the cost is the last round's.
97    pub rounds: usize,
98    /// Merges by structure: a constant, a copy, or a pair of representatives an earlier node has.
99    pub structural: usize,
100    /// Solves under assumptions, the merges they proved, the counterexamples they found and those whose budget ran out.
101    pub queries: usize,
102    /// Solvers the sweep has used.
103    pub solvers: usize,
104    pub merges: usize,
105    pub counterexamples: usize,
106    pub unknown: usize,
107    pub conflicts: u64,
108    /// The solves by the conflicts each took: none, at most 10, at most 100, at most 1000, more.
109    pub by_conflicts: [usize; 5],
110    /// Clauses the solvers learned, those the log let go (nothing a later clause could name rested on them), the
111    /// clauses and hints the log holds, and the steps of the derivation the claims reach.
112    pub learned: usize,
113    pub forgotten: usize,
114    pub log: usize,
115    pub hints: usize,
116    pub proof: usize,
117}
118
119/// A decided miter: a verdict per output name, the derivation behind the equalities, and the search's cost.
120#[derive(Clone, Debug)]
121pub struct Report {
122    pub miter: Miter,
123    pub verdicts: Vec<(String, Verdict)>,
124    pub proof: Lrat,
125    pub stats: Stats,
126}
127
128impl Report {
129    /// Every output proven equal.
130    pub fn all_equal(&self) -> bool {
131        self.verdicts.iter().all(|(_, v)| *v == Verdict::Equal)
132    }
133
134    /// The derivation proves every equality claimed, and every difference's assignment separates its pair.
135    pub fn check(&self) -> bool {
136        let aig = &self.miter.aig;
137        let mut claims: Vec<Vec<Lit>> = Vec::new();
138        for ((_, a, b), (_, v)) in self.miter.pairs.iter().zip(&self.verdicts) {
139            match v {
140                Verdict::Equal if a == b => {}
141                Verdict::Equal if *a == !*b => return false,
142                Verdict::Equal => claims.extend(claim(*a, *b)),
143                Verdict::Differ { inputs, latches } => {
144                    if inputs.len() != aig.inputs().len() || latches.len() != aig.latches().len() {
145                        return false;
146                    }
147                    let values = aig.eval(inputs, latches);
148                    if Aig::value(&values, *a) == Aig::value(&values, *b) {
149                        return false;
150                    }
151                }
152                Verdict::Unknown(_) => {}
153            }
154        }
155        lrat::check_derivation(aig.num_nodes(), &formula(aig), &self.proof, &claims)
156    }
157}
158
159/// An AIG literal as the solver's: node `k` is variable `k`, true when the literal is not complemented.
160fn lit(l: AigLit) -> Lit {
161    Lit::new(l.node(), !l.is_complemented())
162}
163
164/// The two clauses that say `a ≡ b`.
165pub(crate) fn claim(a: AigLit, b: AigLit) -> [Vec<Lit>; 2] {
166    [vec![lit(a).negated(), lit(b)], vec![lit(a), lit(b).negated()]]
167}
168
169/// The miter's formula: for each And node in order its three Tseitin clauses, and node 0 false.
170pub fn formula(aig: &Aig) -> Vec<Vec<Lit>> {
171    let mut clauses = vec![vec![Lit::neg(0)]];
172    for n in 0..aig.num_nodes() as u32 {
173        if let Node::And(x, y) = aig.node(n) {
174            let o = Lit::pos(n);
175            clauses.push(vec![o.negated(), lit(x)]);
176            clauses.push(vec![o.negated(), lit(y)]);
177            clauses.push(vec![o, lit(x).negated(), lit(y).negated()]);
178        }
179    }
180    clauses
181}
182
183/// Pair `left` and `right` by output name and decide every pair.
184pub fn equivalent(left: &Aig, right: &Aig, opts: &Options) -> Result<Report, MiterError> {
185    let miter = miter(left, right)?;
186    let pairs: Vec<(AigLit, AigLit)> = miter.pairs.iter().map(|(_, a, b)| (*a, *b)).collect();
187    let (verdicts, proof, stats) = decide(&miter.aig, &pairs, opts);
188    let verdicts = miter.pairs.iter().map(|p| p.0.clone()).zip(verdicts).collect();
189    Ok(Report { miter, verdicts, proof, stats })
190}
191
192/// Decide `a ≡ b` for every pair of literals of `aig`, latches read as free values: a verdict per pair, the derivation
193/// of every equality from [`formula`], and the cost.
194pub fn decide(aig: &Aig, pairs: &[(AigLit, AigLit)], opts: &Options) -> (Vec<Verdict>, Lrat, Stats) {
195    let rounds = opts.rounds.max(1);
196    let mut budget = opts.budget;
197    for round in 1..=rounds {
198        let last = round == rounds;
199        let pair_budget = if last { opts.final_budget } else { budget.saturating_mul(100) };
200        let (verdicts, proof, stats) = sweep_round(aig, pairs, opts, round, budget, pair_budget);
201        if last || !verdicts.iter().any(|v| matches!(v, Verdict::Unknown(_))) {
202            return (verdicts, proof, stats);
203        }
204        budget = budget.saturating_mul(10);
205    }
206    unreachable!("the last round returns")
207}
208
209/// Round `round`: the sweep at `budget` conflicts a query, then every pair within `pair_budget`.
210fn sweep_round(aig: &Aig, pairs: &[(AigLit, AigLit)], opts: &Options, round: usize, budget: u64, pair_budget: u64) -> (Vec<Verdict>, Lrat, Stats) {
211    let mut e = Engine::new(aig, pairs, opts);
212    e.stats.rounds = round;
213    e.sweep(opts, budget);
214    e.report(opts);
215    let mut verdicts = Vec::with_capacity(pairs.len());
216    let mut claims: Vec<u64> = Vec::new();
217    for &(a, b) in pairs {
218        verdicts.push(e.decide_pair(a, b, pair_budget, &mut claims));
219    }
220    e.sync();
221    e.report(opts);
222    e.stats.log = e.log.len();
223    e.stats.conflicts += e.solver.conflicts();
224    let proof = e.derivation(&claims);
225    e.stats.proof = proof.steps.len();
226    (verdicts, proof, e.stats)
227}
228
229/// Simulation values: `words` 64-bit words for each node simulated (the nodes the pairs reach: no other is ever asked
230/// for), the last word filling with counterexamples.
231struct Sim {
232    words: usize,
233    inputs: Vec<u64>,
234    latches: Vec<u64>,
235    /// The nodes simulated in increasing order, and each node's row of values (`u32::MAX` for one not simulated).
236    nodes: Vec<u32>,
237    row: Vec<u32>,
238    values: Vec<u64>,
239    /// Patterns of the last word taken by counterexamples (64 once it is full).
240    used: usize,
241}
242
243impl Sim {
244    /// Random values for every input and latch, the same whichever nodes are simulated, and `nodes` (closed under
245    /// fan-in, in increasing order) simulated over them.
246    fn random(aig: &Aig, words: usize, seed: u64, nodes: Vec<u32>) -> Sim {
247        let mut state = seed;
248        let mut next = || {
249            state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
250            let mut z = state;
251            z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
252            z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
253            z ^ (z >> 31)
254        };
255        let inputs: Vec<u64> = (0..aig.inputs().len() * words).map(|_| next()).collect();
256        let latches: Vec<u64> = (0..aig.latches().len() * words).map(|_| next()).collect();
257        let mut row = vec![u32::MAX; aig.num_nodes()];
258        for (i, &n) in nodes.iter().enumerate() {
259            row[n as usize] = i as u32;
260        }
261        let mut sim = Sim { words, inputs, latches, nodes, row, values: Vec::new(), used: 64 };
262        sim.run(aig);
263        sim
264    }
265
266    /// Simulate the nodes held over the inputs' and latches' values.
267    fn run(&mut self, aig: &Aig) {
268        let words = self.words;
269        let mut v = vec![0u64; self.nodes.len() * words];
270        for (i, &n) in self.nodes.iter().enumerate() {
271            match aig.node(n) {
272                Node::False => {}
273                Node::Input(k) => v[i * words..(i + 1) * words].copy_from_slice(&self.inputs[k * words..(k + 1) * words]),
274                Node::Latch(k) => v[i * words..(i + 1) * words].copy_from_slice(&self.latches[k * words..(k + 1) * words]),
275                Node::And(a, b) => {
276                    let (ra, rb) = (self.row[a.node() as usize] as usize, self.row[b.node() as usize] as usize);
277                    let (ma, mb) = (if a.is_complemented() { !0 } else { 0 }, if b.is_complemented() { !0 } else { 0 });
278                    for w in 0..words {
279                        v[i * words + w] = (v[ra * words + w] ^ ma) & (v[rb * words + w] ^ mb);
280                    }
281                }
282            }
283        }
284        self.values = v;
285    }
286
287    /// Add one assignment and simulate again: a fresh word carrying it in every pattern, or the next free pattern of
288    /// the last word.
289    fn add(&mut self, aig: &Aig, inputs: &[bool], latches: &[bool]) {
290        if self.used == 64 {
291            let grow = |old: &[u64], values: &[bool], words: usize| -> Vec<u64> {
292                let mut out = Vec::with_capacity(values.len() * (words + 1));
293                for (i, &v) in values.iter().enumerate() {
294                    out.extend_from_slice(&old[i * words..(i + 1) * words]);
295                    out.push(if v { !0 } else { 0 });
296                }
297                out
298            };
299            self.inputs = grow(&self.inputs, inputs, self.words);
300            self.latches = grow(&self.latches, latches, self.words);
301            self.words += 1;
302            self.used = 1;
303        } else {
304            let (words, bit) = (self.words, self.used);
305            for (store, values) in [(&mut self.inputs, inputs), (&mut self.latches, latches)] {
306                for (i, &v) in values.iter().enumerate() {
307                    let cell = &mut store[i * words + words - 1];
308                    *cell = (*cell & !(1 << bit)) | ((v as u64) << bit);
309                }
310            }
311            self.used += 1;
312        }
313        self.run(aig);
314    }
315
316    /// A node's signature with its phase normalised (complemented so the first pattern reads 0), and that phase.
317    fn signature(&self, n: u32) -> (Vec<u64>, bool) {
318        let at = self.row[n as usize] as usize;
319        let row = &self.values[at * self.words..(at + 1) * self.words];
320        let phase = row[0] & 1 == 1;
321        (row.iter().map(|&w| if phase { !w } else { w }).collect(), phase)
322    }
323}
324
325/// Where each solver numbers the clauses it learns from: above every id the log gives, so the two never meet.
326const LEARNED_IDS: u64 = 1 << 62;
327
328
329/// A clause of the derivation log: its id, its literals in the miter's numbering, its LRAT hints, and whether a solver
330/// learned it.
331struct Step {
332    id: u64,
333    clause: Vec<Lit>,
334    hints: Vec<u64>,
335    learned: bool,
336}
337
338struct Engine<'a> {
339    aig: &'a Aig,
340    formula: Vec<Vec<Lit>>,
341    /// For each And node, the id of the first of its three clauses in the formula.
342    def_id: Vec<u64>,
343    solver: Solver,
344    /// The derivation log after the formula, in id order, the id its next clause takes, and for every id the log has
345    /// given (from the formula's last on) its place in the log plus one, 0 once it is let go.
346    log: Vec<Step>,
347    next_id: u64,
348    at: Vec<u32>,
349    /// Where the current solver's time in the log begins, the log's id for each clause of the solver's a hint can
350    /// still name, and how long the solver's part of the log grows before what nothing can name is let go.
351    segment: usize,
352    learned: HashMap<u64, u64>,
353    collect: usize,
354    collect_at: usize,
355    /// Each node's definition over its fan-ins' representatives by the ids of its three clauses, once a solver has
356    /// needed it.
357    defs: Vec<Option<[u64; 3]>>,
358    /// Each merged node's two clauses with its representative, `(¬n ∨ r)` then `(n ∨ ¬r)`, by id.
359    lemmas: Vec<Option<[u64; 2]>>,
360    /// Per node, the current solver's variable for it plus one (0: none yet), and whether its definition is held.
361    local: Vec<u32>,
362    defined: Vec<bool>,
363    /// Per variable of the current solver, its node.
364    nodes: Vec<u32>,
365    recycle: usize,
366    /// Each node's representative literal: itself, or what it was proven equal to.
367    repr: Vec<AigLit>,
368    /// The node each pair of fan-in representatives was first seen at.
369    strash: HashMap<(AigLit, AigLit), u32>,
370    /// Nodes both sides of the miter reach: a merge of one proves nothing the pairs need, and an invariant of shared
371    /// logic (a carry that never rises) can cost a solver everything, so they are never put to it.
372    shared: HashSet<u32>,
373    /// Per node, whether either side of the pairs reaches it.
374    reached: Vec<bool>,
375    sim: Sim,
376    stats: Stats,
377}
378
379impl<'a> Engine<'a> {
380    fn new(aig: &'a Aig, pairs: &[(AigLit, AigLit)], opts: &Options) -> Engine<'a> {
381        let left: HashSet<u32> = aig.cone(&pairs.iter().map(|p| p.0).collect::<Vec<_>>()).into_iter().collect();
382        let right: HashSet<u32> = aig.cone(&pairs.iter().map(|p| p.1).collect::<Vec<_>>()).into_iter().collect();
383        let mut reached = vec![false; aig.num_nodes()];
384        for &n in left.iter().chain(&right) {
385            reached[n as usize] = true;
386        }
387        let cone: Vec<u32> = (0..aig.num_nodes() as u32).filter(|&n| reached[n as usize]).collect();
388        let formula = formula(aig);
389        let mut def_id = vec![0u64; aig.num_nodes()];
390        let mut next = 2u64;
391        for n in 0..aig.num_nodes() as u32 {
392            if let Node::And(..) = aig.node(n) {
393                def_id[n as usize] = next;
394                next += 3;
395            }
396        }
397        let mut e = Engine {
398            aig,
399            next_id: formula.len() as u64 + 1,
400            at: Vec::new(),
401            formula,
402            def_id,
403            solver: Solver::new(0),
404            log: Vec::new(),
405            segment: 0,
406            learned: HashMap::new(),
407            collect: opts.collect.max(1),
408            collect_at: opts.collect.max(1),
409            defs: vec![None; aig.num_nodes()],
410            lemmas: vec![None; aig.num_nodes()],
411            local: vec![0; aig.num_nodes()],
412            defined: vec![false; aig.num_nodes()],
413            nodes: Vec::new(),
414            recycle: opts.recycle.max(1),
415            repr: (0..aig.num_nodes() as u32).map(|n| AigLit::new(n, false)).collect(),
416            strash: HashMap::new(),
417            shared: left.intersection(&right).copied().collect(),
418            stats: Stats { nodes: aig.num_nodes(), cone: cone.len(), ..Stats::default() },
419            reached,
420            sim: Sim::random(aig, opts.sim_words.max(1), opts.seed, cone),
421        };
422        e.fresh();
423        e
424    }
425
426    /// Where the log holds clause `id`, if it does.
427    fn place(&self, id: u64) -> Option<usize> {
428        let k = id.checked_sub(self.formula.len() as u64 + 1)?;
429        match self.at.get(k as usize) {
430            Some(&p) if p > 0 => Some(p as usize - 1),
431            _ => None,
432        }
433    }
434
435    /// Which steps of the log from `from` on the ids of `roots` reach back through their hints. An id the log does not
436    /// hold there (the formula's, or one from before `from`) ends its path.
437    fn reached(&self, from: usize, roots: impl IntoIterator<Item = u64>) -> Vec<bool> {
438        let mut keep = vec![false; self.log.len() - from];
439        let mut stack: Vec<u64> = roots.into_iter().collect();
440        while let Some(id) = stack.pop() {
441            if let Some(k) = self.place(id).and_then(|p| p.checked_sub(from)) {
442                if !std::mem::replace(&mut keep[k], true) {
443                    stack.extend(&self.log[from + k].hints);
444                }
445            }
446        }
447        keep
448    }
449
450    /// Let go of the current solver's learned clauses that nothing a later clause can name rests on: no clause the
451    /// engine made, and (unless the solver is being replaced) no clause the solver still holds.
452    fn collect(&mut self, replacing: bool) {
453        let start = self.segment;
454        let mut roots: Vec<u64> = self.log[start..].iter().filter(|s| !s.learned).map(|s| s.id).collect();
455        let live: HashSet<u64> = if replacing { HashSet::new() } else { self.solver.live_ids().iter().copied().collect() };
456        self.learned.retain(|local, _| live.contains(local));
457        roots.extend(self.learned.values());
458        let mut keep = self.reached(start, roots).into_iter();
459        let tail = self.log.split_off(start);
460        let held = tail.len();
461        let base = self.formula.len() as u64 + 1;
462        for (step, kept) in tail.into_iter().zip(&mut keep) {
463            let k = (step.id - base) as usize;
464            if kept {
465                self.at[k] = self.log.len() as u32 + 1;
466                self.log.push(step);
467            } else {
468                self.at[k] = 0;
469                self.stats.hints -= step.hints.len();
470            }
471        }
472        self.stats.forgotten += held - (self.log.len() - start);
473        self.collect_at = (2 * (self.log.len() - start)).saturating_add(self.collect);
474    }
475
476    /// A fresh solver holding only node 0, false. The old one's learned clauses are logged first, and those no clause
477    /// the engine made in its time rests on are let go: no later clause can name them.
478    fn fresh(&mut self) {
479        self.sync();
480        self.collect(true);
481        self.segment = self.log.len();
482        self.collect_at = self.collect;
483        self.stats.conflicts += self.solver.conflicts();
484        for &n in &self.nodes {
485            self.local[n as usize] = 0;
486            self.defined[n as usize] = false;
487        }
488        self.nodes.clear();
489        self.learned.clear();
490        self.solver = Solver::new(0);
491        self.solver.set_lrat(true);
492        self.solver.set_next_clause_id(LEARNED_IDS);
493        self.solver.set_decision_vars(&[]);
494        let zero = self.var(0);
495        self.solver.add_clause_with_id(vec![Lit::neg(zero)], 1);
496        self.defined[0] = true;
497        self.stats.solvers += 1;
498    }
499
500    /// The current solver's variable for node `n`, made a decision variable the first time it is asked for.
501    fn var(&mut self, n: u32) -> u32 {
502        if self.local[n as usize] == 0 {
503            let v = self.solver.add_vars(1);
504            self.solver.add_decision_vars(&[v]);
505            self.local[n as usize] = v as u32 + 1;
506            self.nodes.push(n);
507        }
508        self.local[n as usize] - 1
509    }
510
511    /// A literal of the miter as the current solver's, its node given a variable if it had none.
512    fn to_local(&mut self, l: Lit) -> Lit {
513        Lit::new(self.var(l.var()), l.is_positive())
514    }
515
516    /// The clause the formula or the log has under `id`.
517    fn clause_of(&self, id: u64) -> Option<&[Lit]> {
518        if id == 0 {
519            return None;
520        }
521        match self.formula.get(id as usize - 1) {
522            Some(c) => Some(c),
523            None => self.place(id).map(|k| self.log[k].clause.as_slice()),
524        }
525    }
526
527    /// Give the solver the log's clause `id`, under that id.
528    fn give(&mut self, id: u64) {
529        let Some(clause) = self.clause_of(id).map(<[Lit]>::to_vec) else { return };
530        let local: Vec<Lit> = clause.into_iter().map(|l| self.to_local(l)).collect();
531        self.solver.add_clause_with_id(local, id);
532    }
533
534    /// Log `clause` with `hints`; its id.
535    fn push(&mut self, clause: Vec<Lit>, hints: Vec<u64>, learned: bool) -> u64 {
536        let id = self.next_id;
537        self.next_id += 1;
538        self.at.push(self.log.len() as u32 + 1);
539        self.stats.hints += hints.len();
540        self.log.push(Step { id, clause, hints, learned });
541        id
542    }
543
544    /// A hint of the current solver's as the log's id. A learned clause the log has not met maps to 0, which no clause
545    /// has, so the derivation then does not check.
546    fn global(&self, hint: u64) -> u64 {
547        if hint >= LEARNED_IDS {
548            self.learned.get(&hint).copied().unwrap_or(0)
549        } else {
550            hint
551        }
552    }
553
554    /// Log the clauses the solver learned since the log last looked, each with its chain in the log's ids, and let go
555    /// of what nothing can name once the solver's part of the log has doubled.
556    fn sync(&mut self) {
557        let learned = self.solver.take_learned();
558        self.stats.learned += learned.len();
559        for c in learned {
560            let mut clause = c.lits;
561            for l in &mut clause {
562                *l = Lit::new(self.nodes[l.var() as usize], l.is_positive());
563            }
564            let mut hints = c.hints;
565            for h in &mut hints {
566                *h = self.global(*h);
567            }
568            let id = self.push(clause, hints, true);
569            self.learned.insert(c.id, id);
570        }
571        if self.log.len() - self.segment >= self.collect_at {
572            self.collect(false);
573        }
574    }
575
576    /// The LRAT chain of `clause` through `candidates`: under the clause's negation, each candidate in the order it
577    /// turns unit, ending at the first that conflicts. `None` when propagation through them stops short of a conflict.
578    fn chain(&self, clause: &[Lit], candidates: &[u64]) -> Option<Vec<u64>> {
579        let mut value: HashMap<u32, bool> = clause.iter().map(|l| (l.var(), !l.is_positive())).collect();
580        let mut used = vec![false; candidates.len()];
581        let mut hints = Vec::new();
582        loop {
583            let mut progressed = false;
584            for (k, &id) in candidates.iter().enumerate() {
585                let Some(c) = self.clause_of(id).filter(|_| !used[k]) else { continue };
586                let mut open: Option<Lit> = None;
587                let mut quiet = false;
588                for &l in c {
589                    match value.get(&l.var()) {
590                        Some(&v) if v == l.is_positive() => quiet = true,
591                        Some(_) => {}
592                        None if open.is_none() || open == Some(l) => open = Some(l),
593                        None => quiet = true,
594                    }
595                    if quiet {
596                        break;
597                    }
598                }
599                if quiet {
600                    continue;
601                }
602                used[k] = true;
603                hints.push(id);
604                let Some(u) = open else { return Some(hints) };
605                value.insert(u.var(), u.is_positive());
606                progressed = true;
607            }
608            if !progressed {
609                return None;
610            }
611        }
612    }
613
614    /// Log `clause` with its chain through `candidates`; its id. Every clause the engine derives this way follows from
615    /// what it names, so a chain not found is the engine's fault, and the derivation then does not check.
616    fn derive(&mut self, clause: Vec<Lit>, candidates: &[u64]) -> u64 {
617        let hints = self.chain(&clause, candidates).unwrap_or_default();
618        self.sync();
619        self.push(clause, hints, false)
620    }
621
622    /// The ids of what says what node `n` is: its three clauses in the formula, and its merge with its representative.
623    fn about(&self, n: u32) -> impl Iterator<Item = u64> + '_ {
624        let f = self.def_id[n as usize];
625        let definition = matches!(self.aig.node(n), Node::And(..)).then_some([f, f + 1, f + 2]);
626        definition.into_iter().flatten().chain(self.lemmas[n as usize].into_iter().flatten())
627    }
628
629    /// The ids of what says what each of `nodes` and their fan-ins is, and node 0 false.
630    fn around(&self, nodes: &[u32]) -> Vec<u64> {
631        let mut near: Vec<u32> = nodes.to_vec();
632        for &n in nodes {
633            if let Node::And(x, y) = self.aig.node(n) {
634                near.extend([x.node(), y.node()]);
635            }
636        }
637        near.sort_unstable();
638        near.dedup();
639        let mut ids = vec![1];
640        for n in near {
641            ids.extend(self.about(n));
642        }
643        ids
644    }
645
646    /// Give the solver the definitions of every node of the reduced cones of `roots` it does not hold yet: each node's
647    /// three clauses over its fan-ins' representatives, logged with their chains the first time a solver needs them.
648    fn encode(&mut self, roots: &[AigLit]) {
649        let mut stack: Vec<u32> = roots.iter().map(|l| l.node()).collect();
650        while let Some(n) = stack.pop() {
651            self.var(n);
652            if std::mem::replace(&mut self.defined[n as usize], true) {
653                continue;
654            }
655            if let Node::And(x, y) = self.aig.node(n) {
656                let (rx, ry) = (self.resolve(x), self.resolve(y));
657                let ids = match self.defs[n as usize] {
658                    Some(ids) => ids,
659                    None => {
660                        let o = Lit::pos(n);
661                        let near = self.around(&[n]);
662                        let ids = [
663                            self.derive(vec![o.negated(), lit(rx)], &near),
664                            self.derive(vec![o.negated(), lit(ry)], &near),
665                            self.derive(vec![o, lit(rx).negated(), lit(ry).negated()], &near),
666                        ];
667                        self.defs[n as usize] = Some(ids);
668                        ids
669                    }
670                };
671                for id in ids {
672                    self.give(id);
673                }
674                stack.push(rx.node());
675                stack.push(ry.node());
676            }
677        }
678    }
679
680    /// Hand the cost so far to the progress report, if there is one.
681    fn report(&self, opts: &Options) {
682        if let Some(p) = opts.progress {
683            let mut so_far = self.stats.clone();
684            so_far.conflicts += self.solver.conflicts();
685            so_far.log = self.log.len();
686            (p.report)(&so_far);
687        }
688    }
689
690    fn resolve(&self, l: AigLit) -> AigLit {
691        self.repr[l.node() as usize].with_phase(l.is_complemented())
692    }
693
694    /// Record `n ≡ t` by structure, `t` the representative of `n`: both clauses logged with their chains through the
695    /// definitions and merges of `n`, of `m` (the earlier node with the same pair of representatives, when there is
696    /// one) and of their fan-ins. A solver never needs them: it holds the reduced graph, where `n` is `t`.
697    fn merge(&mut self, n: u32, t: AigLit, m: Option<u32>) {
698        let near = self.around(&[n].into_iter().chain(m).collect::<Vec<_>>());
699        let [a, b] = claim(AigLit::new(n, false), t);
700        self.lemmas[n as usize] = Some([self.derive(a, &near), self.derive(b, &near)]);
701        self.repr[n as usize] = t;
702    }
703
704    /// `a ≡ b` put to the solver within `budget`: both clauses, each the negation of the assumptions an unsatisfiable
705    /// solve refuted, logged with the refutation's chain and given to it; a model of the miter separating the pair; or
706    /// the budget run out.
707    fn prove(&mut self, a: AigLit, b: AigLit, budget: u64) -> Proved {
708        if self.nodes.len() >= self.recycle {
709            self.fresh();
710        }
711        self.encode(&[a, b]);
712        let mut ids = [0u64; 2];
713        for (k, (x, y)) in [(a, !b), (!a, b)].into_iter().enumerate() {
714            self.stats.queries += 1;
715            let before = self.solver.conflicts();
716            let assumptions = [self.to_local(lit(x)), self.to_local(lit(y))];
717            let result = self.solver.solve_under_assumptions_budgeted(&assumptions, budget);
718            let took = self.solver.conflicts() - before;
719            self.stats.by_conflicts[[0, 10, 100, 1000].iter().take_while(|&&b| took > b).count()] += 1;
720            match result {
721                BudgetedResult::Unsat => {
722                    self.sync();
723                    let hints: Vec<u64> = self.solver.unsat_hints().iter().map(|&h| self.global(h)).collect();
724                    ids[k] = self.push(vec![lit(x).negated(), lit(y).negated()], hints, false);
725                    self.give(ids[k]);
726                }
727                BudgetedResult::Sat(model) => {
728                    let value = |n: u32| self.local[n as usize] > 0 && model[self.local[n as usize] as usize - 1];
729                    let inputs: Vec<bool> = self.aig.inputs().iter().map(|p| value(p.lit.node())).collect();
730                    let latches: Vec<bool> = (0..self.aig.latches().len()).map(|j| value(self.aig.latch_lit(j).node())).collect();
731                    let values = self.aig.eval(&inputs, &latches);
732                    return if Aig::value(&values, a) != Aig::value(&values, b) {
733                        Proved::Differ(inputs, latches)
734                    } else {
735                        Proved::Unknown("the solver's model does not separate the pair".into())
736                    };
737                }
738                BudgetedResult::Budget => return Proved::Unknown(format!("no verdict within {budget} conflicts")),
739            }
740        }
741        Proved::Equal(ids)
742    }
743
744    /// What node `n` is by structure: a constant, one of its fan-ins' representatives, or the representative of the
745    /// earlier node with the same pair of them (and that node).
746    fn structural(&mut self, n: u32, x: AigLit, y: AigLit) -> Option<(AigLit, Option<u32>)> {
747        let (rx, ry) = (self.resolve(x), self.resolve(y));
748        if rx == AigLit::FALSE || ry == AigLit::FALSE || rx == !ry {
749            return Some((AigLit::FALSE, None));
750        }
751        if rx == AigLit::TRUE || rx == ry {
752            return Some((ry, None));
753        }
754        if ry == AigLit::TRUE {
755            return Some((rx, None));
756        }
757        let key = if rx.code() <= ry.code() { (rx, ry) } else { (ry, rx) };
758        match self.strash.get(&key) {
759            Some(&m) => Some((self.resolve(AigLit::new(m, false)), Some(m))),
760            None => {
761                self.strash.insert(key, n);
762                None
763            }
764        }
765    }
766
767    fn sweep(&mut self, opts: &Options, budget: u64) {
768        let aig = self.aig;
769        // Each simulation class by its normalised signature, its representative the first node seen with it.
770        let mut classes: HashMap<Vec<u64>, u32> = HashMap::new();
771        let mut reps: Vec<u32> = Vec::new();
772        for n in 0..aig.num_nodes() as u32 {
773            if !self.reached[n as usize] {
774                continue;
775            }
776            self.stats.swept += 1;
777            if opts.progress.is_some_and(|p| p.every > 0 && self.stats.swept % p.every == 0) {
778                self.report(opts);
779            }
780            if let Node::And(x, y) = aig.node(n) {
781                if let Some((t, m)) = self.structural(n, x, y) {
782                    self.merge(n, t, m);
783                    self.stats.structural += 1;
784                    continue;
785                }
786            }
787            let candidate = matches!(aig.node(n), Node::And(..)) && !self.shared.contains(&n);
788            let mut attempts = 0;
789            loop {
790                let (key, phase) = self.sim.signature(n);
791                let Some(&r) = classes.get(&key) else {
792                    classes.insert(key, n);
793                    break;
794                };
795                if !candidate || attempts == opts.attempts {
796                    break;
797                }
798                attempts += 1;
799                let (_, phase_r) = self.sim.signature(r);
800                let target = AigLit::new(r, phase ^ phase_r);
801                match self.prove(AigLit::new(n, false), target, budget) {
802                    Proved::Equal(ids) => {
803                        self.lemmas[n as usize] = Some(ids);
804                        self.repr[n as usize] = target;
805                        self.stats.merges += 1;
806                        break;
807                    }
808                    Proved::Differ(inputs, latches) => {
809                        self.stats.counterexamples += 1;
810                        self.sim.add(aig, &inputs, &latches);
811                        classes.clear();
812                        for &m in &reps {
813                            classes.entry(self.sim.signature(m).0).or_insert(m);
814                        }
815                    }
816                    Proved::Unknown(_) => {
817                        self.stats.unknown += 1;
818                        break;
819                    }
820                }
821            }
822            if self.repr[n as usize] == AigLit::new(n, false) {
823                reps.push(n);
824            }
825        }
826    }
827
828    /// One output pair's verdict, the ids of its two claim clauses pushed to `claims` when it is equal: each chained
829    /// through the outputs' merges and, when their representatives differ, the merge proven between those.
830    fn decide_pair(&mut self, a: AigLit, b: AigLit, budget: u64, claims: &mut Vec<u64>) -> Verdict {
831        if a == b {
832            return Verdict::Equal;
833        }
834        let (ra, rb) = (self.resolve(a), self.resolve(b));
835        let mut near: Vec<u64> = vec![1];
836        near.extend(self.lemmas[a.node() as usize].into_iter().flatten());
837        near.extend(self.lemmas[b.node() as usize].into_iter().flatten());
838        if ra != rb {
839            match self.prove(ra, rb, budget) {
840                Proved::Equal(ids) => near.extend(ids),
841                Proved::Differ(inputs, latches) => return Verdict::Differ { inputs, latches },
842                Proved::Unknown(why) => {
843                    self.stats.unknown += 1;
844                    return Verdict::Unknown(why);
845                }
846            }
847        }
848        let [x, y] = claim(a, b);
849        claims.push(self.derive(x, &near));
850        claims.push(self.derive(y, &near));
851        Verdict::Equal
852    }
853
854    /// The steps of the log the claims reach back through their hints, in order.
855    fn derivation(&self, claims: &[u64]) -> Lrat {
856        let keep = self.reached(0, claims.iter().copied());
857        let steps = self
858            .log
859            .iter()
860            .zip(keep)
861            .filter(|(_, k)| *k)
862            .map(|(s, _)| lrat::LratStep { id: s.id, clause: s.clause.clone(), hints: s.hints.clone() })
863            .collect();
864        Lrat { steps }
865    }
866}
867
868/// What [`Engine::prove`] found.
869enum Proved {
870    /// Both clauses logged, by their ids.
871    Equal([u64; 2]),
872    Differ(Vec<bool>, Vec<bool>),
873    Unknown(String),
874}
875
876#[cfg(test)]
877#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/fraig/tests.rs"]
878mod tests;