Skip to main content

logicaffeine_proof/
seq_equiv.rs

1//! Sequential equivalence by unrolling: a pipelined circuit proven equal to its combinational reference at its
2//! latency, or two pipelines of different depths proven to compute the same transaction.
3//!
4//! [`at_latency`] unrolls a circuit's transition relation `latency + 1` times into one combinational graph. The
5//! inputs of clock 0 keep their names, the inputs of clock `k ≥ 1` are named `name@k`, a latch with a free initial
6//! value becomes an input `latch@init`, and an initialized latch becomes its constant; the outputs read at clock
7//! `latency` keep their names. Equality of those outputs with a reference over the clock-0 inputs, for every initial
8//! state and every later input, says exactly that the circuit is a pipeline of that latency for that function: an
9//! output that leans on a stale state, on a later input, or on feedback is told apart by a witness.
10//! [`aligned`] does this for both sides at once, each side's initial state its own (a state shared between two
11//! designs would hide a difference), and decides the pair with [`crate::sweep`], certificate and all;
12//! [`aligned_by_fraig`] decides it by incremental SAT sweeping ([`crate::fraig`]), one derivation for every equality.
13//!
14//! [`always_equal`] decides two sequential circuits for every clock from reset, by register correspondence (van
15//! Eijk, *Sequential equivalence checking based on structural similarities*, TCAD 2000): the two run side by side in
16//! one [`Product`]; simulated from reset, their registers fall into classes of equal (or complementary) values; the
17//! classes are refined until, assumed of a state, they hold of the next state for every input, each step a refuted
18//! difference with an LRAT proof ([`crate::sweep::query`] with the classes as its assumptions); and the outputs are
19//! proven equal under them. The [`InductionCertificate`] is that invariant, its steps' proofs and the outputs' proofs;
20//! [`check_always`] replays it, including the invariant's truth at reset. When the correspondence is not strong
21//! enough, bounded model checking looks for the shortest difference from reset ([`Trace`], replayed by
22//! [`replays`]); finding none, the verdict is unknown, never equal.
23
24use std::collections::HashMap;
25
26use crate::aig::{Aig, AigLit, Init, Leaf, Node};
27use crate::fraig;
28use crate::lrat::{self, Lrat};
29use crate::sweep::{self, MiterError, Options, Outcome, Report};
30
31/// The graph unrolled over `frames` clocks, the latches' starting values from their initial values, the outputs of
32/// every clock named `name@k`.
33pub fn unroll(aig: &Aig, frames: usize) -> Aig {
34    let mut out = Aig::new();
35    let mut frame = Frames::new(aig, &mut out, "@init");
36    for k in 0..frames {
37        let map = frame.step(aig, &mut out, k);
38        for o in aig.outputs() {
39            let lit = at(&map, o.lit);
40            out.add_output(lit, o.name.as_ref().map(|n| format!("{n}@{k}")));
41        }
42        frame.advance(aig, &map);
43    }
44    out
45}
46
47/// The outputs at clock `latency` as a combinational graph over the clock-0 inputs (by their own names), the later
48/// clocks' inputs and the free initial values.
49pub fn at_latency(aig: &Aig, latency: usize) -> Aig {
50    at_latency_with(aig, latency, "@init")
51}
52
53fn at_latency_with(aig: &Aig, latency: usize, init: &str) -> Aig {
54    let mut out = Aig::new();
55    let mut frame = Frames::new(aig, &mut out, init);
56    for k in 0..=latency {
57        let map = frame.step(aig, &mut out, k);
58        if k == latency {
59            for o in aig.outputs() {
60                let lit = at(&map, o.lit);
61                out.add_output(lit, o.name.clone());
62            }
63        } else {
64            frame.advance(aig, &map);
65        }
66    }
67    out
68}
69
70fn at(map: &[AigLit], l: AigLit) -> AigLit {
71    map[l.node() as usize].with_phase(l.is_complemented())
72}
73
74/// The unrolling's running state: the clock-0 inputs, created first so a combinational circuit keeps its order, and
75/// each latch's value at the current clock.
76struct Frames {
77    first: Vec<AigLit>,
78    state: Vec<AigLit>,
79}
80
81impl Frames {
82    fn new(aig: &Aig, out: &mut Aig, init: &str) -> Frames {
83        let first = aig.inputs().iter().map(|p| out.add_input(p.name.clone())).collect();
84        let state = aig
85            .latches()
86            .iter()
87            .map(|l| match l.init {
88                Init::Zero => AigLit::FALSE,
89                Init::One => AigLit::TRUE,
90                Init::Free => out.add_input(l.name.as_ref().map(|n| format!("{n}{init}"))),
91            })
92            .collect();
93        Frames { first, state }
94    }
95
96    /// Copy the circuit at clock `k`; returns the literal each of its nodes became.
97    fn step(&mut self, aig: &Aig, out: &mut Aig, k: usize) -> Vec<AigLit> {
98        let inputs: Vec<AigLit> = if k == 0 {
99            self.first.clone()
100        } else {
101            aig.inputs().iter().map(|p| out.add_input(p.name.as_ref().map(|n| format!("{n}@{k}")))).collect()
102        };
103        let state = &self.state;
104        out.import(aig, &mut |leaf| match leaf {
105            Leaf::Input(i) => inputs[i],
106            Leaf::Latch(j) => state[j],
107        })
108    }
109
110    fn advance(&mut self, aig: &Aig, map: &[AigLit]) {
111        self.state = aig.latches().iter().map(|l| at(map, l.next)).collect();
112    }
113}
114
115/// Whether `left`'s outputs at clock `left_latency` equal `right`'s at clock `right_latency`, as functions of the
116/// clock-0 inputs they share by name, for every initial state of each and every later input.
117pub fn aligned(
118    left: &Aig,
119    left_latency: usize,
120    right: &Aig,
121    right_latency: usize,
122    opts: &Options,
123) -> Result<Report, MiterError> {
124    let (l, r) = aligned_pair(left, left_latency, right, right_latency);
125    sweep::equivalent(&l, &r, opts)
126}
127
128/// The two combinational circuits [`aligned`] decides between: each side at its latency, its free initial values
129/// named apart from the other's (`[email protected]`, `[email protected]`).
130pub fn aligned_pair(left: &Aig, left_latency: usize, right: &Aig, right_latency: usize) -> (Aig, Aig) {
131    (at_latency_with(left, left_latency, "@init.left"), at_latency_with(right, right_latency, "@init.right"))
132}
133
134/// Whether `pipeline`'s outputs at clock `latency` equal the combinational `reference` over the clock-0 inputs.
135pub fn latency_equivalent(pipeline: &Aig, reference: &Aig, latency: usize, opts: &Options) -> Result<Report, MiterError> {
136    aligned(pipeline, latency, reference, 0, opts)
137}
138
139/// [`aligned`], decided by [`crate::fraig`].
140pub fn aligned_by_fraig(
141    left: &Aig,
142    left_latency: usize,
143    right: &Aig,
144    right_latency: usize,
145    opts: &fraig::Options,
146) -> Result<fraig::Report, MiterError> {
147    let (l, r) = aligned_pair(left, left_latency, right, right_latency);
148    fraig::equivalent(&l, &r, opts)
149}
150
151/// [`latency_equivalent`], decided by [`crate::fraig`].
152pub fn latency_equivalent_by_fraig(
153    pipeline: &Aig,
154    reference: &Aig,
155    latency: usize,
156    opts: &fraig::Options,
157) -> Result<fraig::Report, MiterError> {
158    aligned_by_fraig(pipeline, latency, reference, 0, opts)
159}
160
161/// Two sequential circuits run side by side: inputs of the same name shared, every register of each with its initial
162/// value, each named output of the left paired with the right's, and an output a pair's difference.
163#[derive(Clone, Debug)]
164pub struct Product {
165    pub aig: Aig,
166    pub pairs: Vec<(String, AigLit, AigLit)>,
167}
168
169/// The two circuits as one [`Product`].
170pub fn product(left: &Aig, right: &Aig) -> Result<Product, MiterError> {
171    for g in [left, right] {
172        let mut seen = std::collections::HashSet::new();
173        for o in g.outputs() {
174            let name = o.name.as_ref().ok_or(MiterError::Unnamed)?;
175            if !seen.insert(name) {
176                return Err(MiterError::Duplicate(name.clone()));
177            }
178        }
179    }
180    let mut aig = Aig::new();
181    let mut by_name: HashMap<String, AigLit> = HashMap::new();
182    let left_inputs: Vec<AigLit> = left
183        .inputs()
184        .iter()
185        .map(|p| {
186            let l = aig.add_input(p.name.clone());
187            if let Some(n) = &p.name {
188                by_name.insert(n.clone(), l);
189            }
190            l
191        })
192        .collect();
193    let right_inputs: Vec<AigLit> = right
194        .inputs()
195        .iter()
196        .map(|p| match p.name.as_ref().and_then(|n| by_name.get(n)) {
197            Some(&l) => l,
198            None => aig.add_input(p.name.clone()),
199        })
200        .collect();
201    let side = |g: &Aig, inputs: &[AigLit], tag: &str, aig: &mut Aig| -> Vec<AigLit> {
202        let base = aig.latches().len();
203        let states: Vec<AigLit> = g
204            .latches()
205            .iter()
206            .map(|l| {
207                let k = aig.add_latch(l.name.as_ref().map(|n| format!("{tag}.{n}")), l.init);
208                aig.latch_lit(k)
209            })
210            .collect();
211        let map = aig.import(g, &mut |leaf| match leaf {
212            Leaf::Input(i) => inputs[i],
213            Leaf::Latch(j) => states[j],
214        });
215        for (j, l) in g.latches().iter().enumerate() {
216            aig.set_next(base + j, at(&map, l.next));
217        }
218        map
219    };
220    let lmap = side(left, &left_inputs, "left", &mut aig);
221    let rmap = side(right, &right_inputs, "right", &mut aig);
222    let mut pairs = Vec::with_capacity(left.outputs().len());
223    for o in left.outputs() {
224        let name = o.name.clone().expect("named above");
225        let k = right.output_index(&name).ok_or_else(|| MiterError::Unmatched(name.clone()))?;
226        pairs.push((name, at(&lmap, o.lit), at(&rmap, right.outputs()[k].lit)));
227    }
228    if let Some(o) = right.outputs().iter().find(|o| left.output_index(o.name.as_deref().unwrap()).is_none()) {
229        return Err(MiterError::Unmatched(o.name.clone().unwrap()));
230    }
231    for (name, a, b) in &pairs {
232        let d = aig.xor(*a, *b);
233        aig.add_output(d, Some(name.clone()));
234    }
235    Ok(Product { aig, pairs })
236}
237
238/// How [`always_equal`] searches.
239#[derive(Clone, Debug)]
240pub struct SeqOptions {
241    /// Clocks simulated from reset to draw the register classes from.
242    pub sim_cycles: usize,
243    /// Words of 64 parallel input streams the simulation runs.
244    pub sim_words: usize,
245    pub seed: u64,
246    /// The deepest difference from reset bounded model checking looks for.
247    pub bmc_depth: usize,
248    /// Conflicts a query may spend; 0 is unlimited.
249    pub budget: u64,
250}
251
252impl Default for SeqOptions {
253    fn default() -> SeqOptions {
254        SeqOptions { sim_cycles: 64, sim_words: 4, seed: 0x5eed_c0de_f00d_a16e, bmc_depth: 32, budget: 20_000 }
255    }
256}
257
258/// An assignment from reset under which a pair of outputs differs at the last clock: the registers that start free,
259/// by index of the product's registers (the others are read from their initial values), and the inputs of each clock.
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct Trace {
262    pub init: Vec<bool>,
263    pub frames: Vec<Vec<bool>>,
264}
265
266/// The proof that two circuits agree at every clock from reset: an invariant of register literal pairs true at reset,
267/// each pair's next values proven equal assuming the invariant (one proof a pair, in order), and each output pair
268/// proven equal assuming it (one proof an output, in the product's order, absent for an output pair that is one
269/// literal).
270#[derive(Clone, Debug)]
271pub struct InductionCertificate {
272    pub invariant: Vec<(AigLit, AigLit)>,
273    pub steps: Vec<Lrat>,
274    pub outputs: Vec<Option<Lrat>>,
275}
276
277#[derive(Clone, Debug)]
278pub enum SeqVerdict {
279    Equal(InductionCertificate),
280    Differ(Trace),
281    Unknown(String),
282}
283
284#[derive(Clone, Debug)]
285pub struct SeqReport {
286    pub product: Product,
287    pub verdict: SeqVerdict,
288}
289
290/// The value a register literal (or a constant) holds before the first clock, or `None` when the register starts free.
291fn reset_value(aig: &Aig, l: AigLit) -> Option<bool> {
292    let v = match aig.node(l.node()) {
293        Node::False => false,
294        Node::Latch(k) => match aig.latches()[k].init {
295            Init::Zero => false,
296            Init::One => true,
297            Init::Free => return None,
298        },
299        _ => return None,
300    };
301    Some(v ^ l.is_complemented())
302}
303
304/// The next value of a register literal (a constant's is itself).
305fn next(aig: &Aig, l: AigLit) -> AigLit {
306    match aig.node(l.node()) {
307        Node::Latch(k) => aig.latches()[k].next.with_phase(l.is_complemented()),
308        _ => l,
309    }
310}
311
312/// Whether `cert` proves the product's outputs equal at every clock from reset.
313pub fn check_always(product: &Product, cert: &InductionCertificate) -> bool {
314    let aig = &product.aig;
315    let n = aig.num_nodes();
316    let register = |l: AigLit| (l.node() as usize) < n && matches!(aig.node(l.node()), Node::Latch(_) | Node::False);
317    if cert.steps.len() != cert.invariant.len() || cert.outputs.len() != product.pairs.len() {
318        return false;
319    }
320    for &(a, b) in &cert.invariant {
321        if !register(a) || !register(b) {
322            return false;
323        }
324        match (reset_value(aig, a), reset_value(aig, b)) {
325            (Some(x), Some(y)) if x == y => {}
326            _ => return false,
327        }
328    }
329    for (&(a, b), proof) in cert.invariant.iter().zip(&cert.steps) {
330        let (na, nb) = (next(aig, a), next(aig, b));
331        if na == nb {
332            continue;
333        }
334        let q = sweep::query(aig, na, nb, &cert.invariant);
335        if !lrat::check(q.num_vars, &q.clauses, proof) {
336            return false;
337        }
338    }
339    for ((_, l, r), proof) in product.pairs.iter().zip(&cert.outputs) {
340        match proof {
341            None if l == r => {}
342            None => return false,
343            Some(proof) => {
344                let q = sweep::query(aig, *l, *r, &cert.invariant);
345                if !lrat::check(q.num_vars, &q.clauses, proof) {
346                    return false;
347                }
348            }
349        }
350    }
351    true
352}
353
354/// Whether `trace`, run on the product from reset, ends at a clock where some output pair differs.
355pub fn replays(product: &Product, trace: &Trace) -> bool {
356    let aig = &product.aig;
357    if trace.frames.is_empty() || trace.init.len() != aig.latches().len() {
358        return false;
359    }
360    let mut state: Vec<bool> = aig
361        .latches()
362        .iter()
363        .zip(&trace.init)
364        .map(|(l, &free)| match l.init {
365            Init::Zero => false,
366            Init::One => true,
367            Init::Free => free,
368        })
369        .collect();
370    for (f, inputs) in trace.frames.iter().enumerate() {
371        if inputs.len() != aig.inputs().len() {
372            return false;
373        }
374        let v = aig.eval(inputs, &state);
375        if f + 1 == trace.frames.len() {
376            return product.pairs.iter().any(|(_, l, r)| Aig::value(&v, *l) != Aig::value(&v, *r));
377        }
378        state = aig.latches().iter().map(|l| Aig::value(&v, l.next)).collect();
379    }
380    false
381}
382
383/// Register signatures over `cycles` clocks from reset: each register's words, clock after clock.
384fn register_signatures(aig: &Aig, opts: &SeqOptions) -> Vec<Vec<u64>> {
385    let words = opts.sim_words.max(1);
386    let mut rng = opts.seed;
387    let mut next_word = || {
388        rng = rng.wrapping_add(0x9e37_79b9_7f4a_7c15);
389        let mut z = rng;
390        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
391        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
392        z ^ (z >> 31)
393    };
394    let mut state: Vec<u64> = Vec::with_capacity(aig.latches().len() * words);
395    for l in aig.latches() {
396        for _ in 0..words {
397            state.push(match l.init {
398                Init::Zero => 0,
399                Init::One => !0,
400                Init::Free => next_word(),
401            });
402        }
403    }
404    let mut sigs = vec![Vec::with_capacity(opts.sim_cycles * words); aig.num_nodes()];
405    for _ in 0..opts.sim_cycles.max(1) {
406        let inputs: Vec<u64> = (0..aig.inputs().len() * words).map(|_| next_word()).collect();
407        let sim = aig.simulate(words, &inputs, &state);
408        for (k, l) in aig.latches().iter().enumerate() {
409            sigs[l.node as usize].extend_from_slice(&state[k * words..(k + 1) * words]);
410        }
411        let mut next_state = Vec::with_capacity(state.len());
412        for l in aig.latches() {
413            for w in 0..words {
414                next_state.push(Aig::word(&sim, words, l.next, w));
415            }
416        }
417        state = next_state;
418    }
419    sigs
420}
421
422/// The register literal pairs simulation suggests: each register paired with the first register (or the constant)
423/// whose signature matches its own or its complement, and that starts with a known value.
424fn candidates(aig: &Aig, sigs: &[Vec<u64>]) -> Vec<(AigLit, AigLit)> {
425    let mut classes: HashMap<Vec<u64>, AigLit> = HashMap::new();
426    let len = sigs.iter().map(|s| s.len()).max().unwrap_or(0);
427    classes.insert(vec![0; len], AigLit::FALSE);
428    let mut pairs = Vec::new();
429    for l in aig.latches() {
430        if l.init == Init::Free {
431            continue;
432        }
433        let sig = &sigs[l.node as usize];
434        let phase = sig.first().is_some_and(|w| w & 1 == 1);
435        let key: Vec<u64> = sig.iter().map(|&w| if phase { !w } else { w }).collect();
436        let lit = AigLit::new(l.node, false);
437        match classes.get(&key) {
438            Some(&rep) => pairs.push((lit, rep.with_phase(phase))),
439            None => {
440                classes.insert(key, lit.with_phase(phase));
441            }
442        }
443    }
444    pairs
445}
446
447/// Whether the two circuits' outputs agree at every clock from reset.
448pub fn always_equal(left: &Aig, right: &Aig, opts: &SeqOptions) -> Result<SeqReport, MiterError> {
449    let product = product(left, right)?;
450    let verdict = decide_always(&product, opts);
451    Ok(SeqReport { product, verdict })
452}
453
454fn decide_always(product: &Product, opts: &SeqOptions) -> SeqVerdict {
455    let aig = &product.aig;
456    let sigs = register_signatures(aig, opts);
457    let mut invariant = candidates(aig, &sigs);
458    let steps = 'fixpoint: loop {
459        let mut steps = Vec::with_capacity(invariant.len());
460        for (i, &(a, b)) in invariant.iter().enumerate() {
461            let (na, nb) = (next(aig, a), next(aig, b));
462            if na == nb {
463                steps.push(Lrat { steps: Vec::new() });
464                continue;
465            }
466            match sweep::prove(aig, na, nb, &invariant, opts.budget) {
467                Outcome::Equal(proof) => steps.push(proof),
468                Outcome::Differ(..) | Outcome::Unknown(_) => {
469                    invariant.remove(i);
470                    continue 'fixpoint;
471                }
472            }
473        }
474        break steps;
475    };
476    let mut outputs = Vec::with_capacity(product.pairs.len());
477    let mut inductive = true;
478    for (_, l, r) in &product.pairs {
479        if l == r {
480            outputs.push(None);
481            continue;
482        }
483        match sweep::prove(aig, *l, *r, &invariant, opts.budget) {
484            Outcome::Equal(proof) => outputs.push(Some(proof)),
485            _ => {
486                inductive = false;
487                break;
488            }
489        }
490    }
491    if inductive {
492        let cert = InductionCertificate { invariant, steps, outputs };
493        if check_always(product, &cert) {
494            return SeqVerdict::Equal(cert);
495        }
496        return SeqVerdict::Unknown("the induction certificate did not replay".into());
497    }
498    bmc(product, opts)
499}
500
501/// The shortest difference from reset within `opts.bmc_depth` clocks: the product unrolled once, each clock's
502/// differences joined into one literal and searched for a witness, earliest clock first.
503fn bmc(product: &Product, opts: &SeqOptions) -> SeqVerdict {
504    let aig = &product.aig;
505    let free: Vec<usize> = (0..aig.latches().len()).filter(|&k| aig.latches()[k].init == Init::Free).collect();
506    let n_in = aig.inputs().len();
507    let pairs = product.pairs.len();
508    let mut unrolled = unroll(aig, opts.bmc_depth);
509    let frames: Vec<AigLit> = (0..opts.bmc_depth)
510        .map(|f| {
511            let outs: Vec<AigLit> = unrolled.outputs()[f * pairs..(f + 1) * pairs].iter().map(|o| o.lit).collect();
512            outs.into_iter().fold(AigLit::FALSE, |acc, d| unrolled.or(acc, d))
513        })
514        .collect();
515    for (f, &any) in frames.iter().enumerate() {
516        match sweep::witness(&unrolled, any, opts.budget) {
517            Ok(Some((inputs, _))) => {
518                let mut init = vec![false; aig.latches().len()];
519                for (j, &k) in free.iter().enumerate() {
520                    init[k] = inputs[n_in + j];
521                }
522                let trace_frames = (0..=f)
523                    .map(|c| {
524                        let base = if c == 0 { 0 } else { n_in + free.len() + (c - 1) * n_in };
525                        inputs[base..base + n_in].to_vec()
526                    })
527                    .collect();
528                let trace = Trace { init, frames: trace_frames };
529                return if replays(product, &trace) {
530                    SeqVerdict::Differ(trace)
531                } else {
532                    SeqVerdict::Unknown("a difference the product does not replay".into())
533                };
534            }
535            Ok(None) => {}
536            Err(why) => return SeqVerdict::Unknown(why),
537        }
538    }
539    SeqVerdict::Unknown(format!(
540        "the register correspondence does not prove the outputs, and no difference shows within {} clocks",
541        opts.bmc_depth
542    ))
543}
544
545#[cfg(test)]
546#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/seq_equiv/tests.rs"]
547mod tests;