Skip to main content

logicaffeine_proof/
units.rs

1//! Equivalence over units proven apart: two circuits built of units (adders, multipliers, any module already proven
2//! equal to its counterpart), each unit cut out of its circuit — its results free inputs, its operands outputs — and
3//! the two decided over the units as functions they know nothing of, a unit's own proof standing for what it computes.
4//!
5//! Each side is unrolled to its latency ([`Side`]): its own outputs (those not named under a unit's instance) read at
6//! the last clock, each unit's operands at every clock, each unit's results a free input at every clock, named apart by
7//! side (`left:` and `right:`) so no two units are one unless proven so. An input with no name (an undefined bit) is a
8//! fresh one of its own side at every clock, and a latch with no name starts from a fresh value of its own. A unit applied at clock `t` has its results at
9//! clock `t + latency`: a pipelined unit's proof says its outputs that many clocks late are the function of its
10//! operands.
11//!
12//! A unit's function is its name: a side's units of one function compute one function, and a left function is the
13//! right one `functions` pairs it with (each pair a proof made apart), so the functions fall into classes, a side's
14//! own names and the declared pairs joined.
15//!
16//! The decision runs in rounds. Each builds one miter of both sides with every identification so far and simulates
17//! it; the applications of one class whose operands simulate alike are a group, on either side or both. Each
18//! application of a group is put to [`crate::fraig`] against the group's first one it has not been refuted against: a
19//! pair whose operands it proves equal, bit for bit, is identified for the next round, the later application's results
20//! made the earlier's (functional consistency: one function of equal operands is equal), and a refuted one tries the
21//! group's next. A round that identifies nothing more is the last, and decides the circuits' own outputs. An output
22//! pair that differs over units the decision knows nothing of may still agree over the real ones, so it is
23//! [`Verdict::Unknown`], never a difference.
24//!
25//! [`Report::check`] builds every round's miter again, replays its derivation from the miter's formula, finds in it
26//! the operand equalities of every identification the next round makes, and in the last the outputs' equalities; an
27//! application made one twice, or made one with itself through others, does not check.
28
29use std::collections::{BTreeMap, HashMap, HashSet};
30
31use crate::aig::{Aig, AigLit, Init, Leaf};
32use crate::fraig;
33use crate::lrat::{self, Lrat};
34use crate::sweep::{MiterError, Verdict};
35
36/// A unit of a cut circuit: its instance name, the function it computes (its module), the clocks its results follow
37/// its operands by, and the names of its operand bits (outputs of the cut circuit) and result bits (inputs of it).
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Unit {
40    pub name: String,
41    pub function: String,
42    pub latency: usize,
43    pub operands: Vec<String>,
44    pub results: Vec<String>,
45}
46
47/// One side: its cut circuit, the clock its own outputs are read at, and its units.
48#[derive(Clone, Copy, Debug)]
49pub struct Side<'a> {
50    pub aig: &'a Aig,
51    pub latency: usize,
52    pub units: &'a [Unit],
53}
54
55/// What a side's named input is at a clock: one input both sides share by its name, a constant the environment holds
56/// it at, or a fresh input of the side's own.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum Binding {
59    Shared(String),
60    Const(bool),
61    Fresh,
62}
63
64/// An output bit a side claims at a clock, under the name its counterpart on the other side is claimed by.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct Claim {
67    pub name: String,
68    pub output: String,
69    pub frame: usize,
70}
71
72/// A side unrolled `frames` clocks: what each named input is at each clock (`bind`), the output bits it claims, and its
73/// units. An input with no name is a fresh one of the side's own at every clock, whatever `bind` says.
74pub struct Unrolled<'a> {
75    pub aig: &'a Aig,
76    pub frames: usize,
77    pub bind: &'a dyn Fn(&str, usize) -> Binding,
78    pub claims: &'a [Claim],
79    pub units: &'a [Unit],
80}
81
82/// A port of a unit's instance that is no operand (a pipelined unit's clock) is the unit's, not the circuit's.
83fn of_a_unit(units: &[Unit], name: &str) -> bool {
84    units.iter().any(|u| name.strip_prefix(u.name.as_str()).is_some_and(|rest| rest.starts_with('.')))
85}
86
87/// A side read at its latency as an unrolled one: its inputs at clock `k ≥ 1` named `name@k`, at clock 0 by their own
88/// names; its own outputs (no unit's operand or port) claimed at the latency by their names.
89fn aligned_claims(side: &Side) -> Vec<Claim> {
90    let operands: std::collections::HashSet<&str> = side.units.iter().flat_map(|u| u.operands.iter().map(String::as_str)).collect();
91    side.aig
92        .outputs()
93        .iter()
94        .filter_map(|o| o.name.clone())
95        .filter(|n| !operands.contains(n.as_str()) && !of_a_unit(side.units, n))
96        .map(|n| Claim { name: n.clone(), output: n, frame: side.latency })
97        .collect()
98}
99
100fn aligned_bind(name: &str, k: usize) -> Binding {
101    if k == 0 {
102        Binding::Shared(name.to_string())
103    } else {
104        Binding::Shared(format!("{name}@{k}"))
105    }
106}
107
108/// A unit of a side applied at a clock: the side (0 left, 1 right), the unit's index, the clock of its operands.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
110pub struct Application {
111    pub side: usize,
112    pub unit: usize,
113    pub frame: usize,
114}
115
116/// A round: the identifications its derivation justifies (an application kept, and one whose results become the kept
117/// one's), and the derivation, from the round's miter, of their operands' equalities — in the last round, of the
118/// outputs' equalities.
119#[derive(Clone, Debug)]
120pub struct Round {
121    pub substituted: Vec<(Application, Application)>,
122    pub proof: Lrat,
123}
124
125/// A decision over units: a verdict per output name, the rounds that made it, and the last round's cost.
126#[derive(Clone, Debug)]
127pub struct Report {
128    pub verdicts: Vec<(String, Verdict)>,
129    pub rounds: Vec<Round>,
130    pub stats: fraig::Stats,
131}
132
133/// The miter of both sides under the identifications `subst` (each right result bit's name, the left one it is).
134struct Miter {
135    aig: Aig,
136    outputs: Vec<(String, AigLit, AigLit)>,
137    /// Each application's operand literals, by application.
138    operands: BTreeMap<Application, Vec<AigLit>>,
139}
140
141const TAGS: [&str; 2] = ["left", "right"];
142
143/// The name of side `side`'s result bit `bit` at clock `k`.
144fn result_name(side: usize, bit: &str, k: usize) -> String {
145    format!("{}:{bit}@{k}", TAGS[side])
146}
147
148fn build(sides: [&Unrolled; 2], subst: &HashMap<String, String>) -> Result<Miter, MiterError> {
149    let mut aig = Aig::new();
150    let mut inputs: HashMap<String, AigLit> = HashMap::new();
151    let mut input = |aig: &mut Aig, name: String| -> AigLit { *inputs.entry(name.clone()).or_insert_with(|| aig.add_input(Some(name))) };
152    let mut own: [BTreeMap<String, AigLit>; 2] = [BTreeMap::new(), BTreeMap::new()];
153    let mut operands: BTreeMap<Application, Vec<AigLit>> = BTreeMap::new();
154    for (s, side) in sides.iter().enumerate() {
155        let g = side.aig;
156        let result_bits: HashMap<&str, ()> = side.units.iter().flat_map(|u| u.results.iter().map(|r| (r.as_str(), ()))).collect();
157        let operand_of: HashMap<&str, (usize, usize)> = side
158            .units
159            .iter()
160            .enumerate()
161            .flat_map(|(u, unit)| unit.operands.iter().enumerate().map(move |(i, o)| (o.as_str(), (u, i))))
162            .collect();
163        for unit in side.units {
164            for name in unit.operands.iter().chain(&unit.results) {
165                if g.input_index(name).is_none() && g.output_index(name).is_none() {
166                    return Err(MiterError::Unmatched(format!("{}: {name}", TAGS[s])));
167                }
168            }
169        }
170        let mut claimed: HashMap<(&str, usize), Vec<&str>> = HashMap::new();
171        for c in side.claims {
172            if g.output_index(&c.output).is_none() || c.frame >= side.frames {
173                return Err(MiterError::Unmatched(format!("{}: {} at clock {}", TAGS[s], c.output, c.frame)));
174            }
175            claimed.entry((c.output.as_str(), c.frame)).or_default().push(c.name.as_str());
176        }
177        let mut state: Vec<AigLit> = g
178            .latches()
179            .iter()
180            .map(|l| match (l.init, &l.name) {
181                (Init::Zero, _) => AigLit::FALSE,
182                (Init::One, _) => AigLit::TRUE,
183                (Init::Free, Some(name)) => input(&mut aig, format!("{}:{name}@init", TAGS[s])),
184                (Init::Free, None) => aig.add_input(None),
185            })
186            .collect();
187        for k in 0..side.frames {
188            let frame_inputs: Vec<AigLit> = g
189                .inputs()
190                .iter()
191                .map(|p| {
192                    let Some(name) = p.name.clone() else { return aig.add_input(None) };
193                    if result_bits.contains_key(name.as_str()) {
194                        return input(&mut aig, resolve(subst, &result_name(s, &name, k)));
195                    }
196                    match (side.bind)(&name, k) {
197                        Binding::Shared(symbol) => input(&mut aig, symbol),
198                        Binding::Const(b) => if b { AigLit::TRUE } else { AigLit::FALSE },
199                        Binding::Fresh => aig.add_input(None),
200                    }
201                })
202                .collect();
203            let map = aig.import(g, &mut |leaf| match leaf {
204                Leaf::Input(i) => frame_inputs[i],
205                Leaf::Latch(j) => state[j],
206            });
207            let at = |l: AigLit| map[l.node() as usize].with_phase(l.is_complemented());
208            for o in g.outputs() {
209                let name = o.name.clone().unwrap_or_default();
210                if let Some(&(u, i)) = operand_of.get(name.as_str()) {
211                    let app = Application { side: s, unit: u, frame: k };
212                    let width = side.units[u].operands.len();
213                    operands.entry(app).or_insert_with(|| vec![AigLit::FALSE; width])[i] = at(o.lit);
214                }
215                for claim in claimed.get(&(name.as_str(), k)).into_iter().flatten() {
216                    own[s].insert(claim.to_string(), at(o.lit));
217                }
218            }
219            state = g.latches().iter().map(|l| at(l.next)).collect();
220        }
221        operands.retain(|app, _| app.side != s || app.frame + side.units[app.unit].latency < side.frames);
222    }
223    let mut outputs = Vec::new();
224    for (name, &l) in &own[0] {
225        let &r = own[1].get(name).ok_or_else(|| MiterError::Unmatched(name.clone()))?;
226        outputs.push((name.clone(), l, r));
227    }
228    if let Some(name) = own[1].keys().find(|n| !own[0].contains_key(*n)) {
229        return Err(MiterError::Unmatched(name.clone()));
230    }
231    Ok(Miter { aig, outputs, operands })
232}
233
234/// The result bit a name stands for: each identified bit's name followed to the bit it was made.
235fn resolve(subst: &HashMap<String, String>, name: &str) -> String {
236    let mut at = name;
237    while let Some(next) = subst.get(at) {
238        at = next;
239    }
240    at.to_string()
241}
242
243fn unit<'a>(sides: [&'a Unrolled; 2], app: Application) -> &'a Unit {
244    &sides[app.side].units[app.unit]
245}
246
247/// The identification of `made`'s application with `kept`'s: each of `made`'s result bits named the bit `kept`'s
248/// stands for. An application made one already, or one `kept` already stands for, is refused, so the names stay a
249/// forest and every name resolves.
250fn identify(sides: [&Unrolled; 2], subst: &mut HashMap<String, String>, kept: Application, made: Application) -> bool {
251    let (ku, mu) = (unit(sides, kept), unit(sides, made));
252    let bits: Vec<(String, String)> = ku
253        .results
254        .iter()
255        .zip(&mu.results)
256        .map(|(k, m)| (result_name(made.side, m, made.frame + mu.latency), resolve(subst, &result_name(kept.side, k, kept.frame + ku.latency))))
257        .collect();
258    if bits.iter().any(|(from, to)| from == to || subst.contains_key(from)) {
259        return false;
260    }
261    subst.extend(bits);
262    true
263}
264
265/// The functions of both sides in classes: a side's units of one name are one function, and each pair `functions`
266/// declares makes its left function and its right one one.
267struct Classes(HashMap<(usize, String), usize>);
268
269impl Classes {
270    fn new(sides: [&Unrolled; 2], functions: &[(String, String)]) -> Classes {
271        let mut index: HashMap<(usize, String), usize> = HashMap::new();
272        for (s, side) in sides.iter().enumerate() {
273            for u in side.units {
274                let next = index.len();
275                index.entry((s, u.function.clone())).or_insert(next);
276            }
277        }
278        let mut parent: Vec<usize> = (0..index.len()).collect();
279        fn root(parent: &mut [usize], mut x: usize) -> usize {
280            while parent[x] != x {
281                parent[x] = parent[parent[x]];
282                x = parent[x];
283            }
284            x
285        }
286        for (l, r) in functions {
287            if let (Some(&a), Some(&b)) = (index.get(&(0, l.clone())), index.get(&(1, r.clone()))) {
288                let (ra, rb) = (root(&mut parent, a), root(&mut parent, b));
289                parent[ra.max(rb)] = ra.min(rb);
290            }
291        }
292        Classes(index.into_iter().map(|(k, i)| (k, root(&mut parent, i))).collect())
293    }
294
295    fn of(&self, sides: [&Unrolled; 2], app: Application) -> usize {
296        self.0[&(app.side, unit(sides, app).function.clone())]
297    }
298
299    /// Whether `a` and `b` apply one function, with operands and results of one shape.
300    fn one(&self, sides: [&Unrolled; 2], a: Application, b: Application) -> bool {
301        let (au, bu) = (unit(sides, a), unit(sides, b));
302        self.of(sides, a) == self.of(sides, b) && au.operands.len() == bu.operands.len() && au.results.len() == bu.results.len()
303    }
304}
305
306/// Each application's operands as simulation words, over random inputs.
307fn signatures(m: &Miter, words: usize, seed: u64) -> BTreeMap<Application, Vec<u64>> {
308    let mut state = seed | 1;
309    let mut next = move || {
310        state ^= state << 13;
311        state ^= state >> 7;
312        state ^= state << 17;
313        state
314    };
315    let inputs: Vec<u64> = (0..m.aig.inputs().len() * words).map(|_| next()).collect();
316    let values = m.aig.simulate(words, &inputs, &[]);
317    let word = |l: AigLit, w: usize| {
318        let v = values[l.node() as usize * words + w];
319        if l.is_complemented() {
320            !v
321        } else {
322            v
323        }
324    };
325    m.operands.iter().map(|(app, lits)| (*app, lits.iter().flat_map(|&l| (0..words).map(move |w| word(l, w))).collect())).collect()
326}
327
328/// Decide `left` against `right` over their units, the functions of `functions` (each a left function, the right one
329/// proven equal to it) the only ones identified.
330pub fn decide(left: &Side, right: &Side, functions: &[(String, String)], opts: &fraig::Options) -> Result<Report, MiterError> {
331    let (lc, rc) = (aligned_claims(left), aligned_claims(right));
332    let l = Unrolled { aig: left.aig, frames: left.latency + 1, bind: &aligned_bind, claims: &lc, units: left.units };
333    let r = Unrolled { aig: right.aig, frames: right.latency + 1, bind: &aligned_bind, claims: &rc, units: right.units };
334    decide_unrolled(&l, &r, functions, opts)
335}
336
337/// The applications not yet made one with another, in groups of one class whose operands simulate alike, each group
338/// in order and the groups ordered by their first.
339fn groups(sides: [&Unrolled; 2], classes: &Classes, m: &Miter, made: &HashSet<Application>, opts: &fraig::Options) -> Vec<Vec<Application>> {
340    let sig = signatures(m, opts.sim_words.max(1), opts.seed);
341    let mut by: HashMap<(usize, usize, usize, &[u64]), Vec<Application>> = HashMap::new();
342    for (&app, words) in &sig {
343        if made.contains(&app) {
344            continue;
345        }
346        let u = unit(sides, app);
347        by.entry((classes.of(sides, app), u.operands.len(), u.results.len(), words.as_slice())).or_default().push(app);
348    }
349    let mut groups: Vec<Vec<Application>> = by.into_values().filter(|g| g.len() > 1).collect();
350    groups.sort();
351    groups
352}
353
354/// The identifications one miter proves: each application of a group put against the group's first it has not been
355/// refuted against, a refuted one trying the next until one is proven or none is left; the pairs proven and their
356/// derivation, or none.
357fn identifications(m: &Miter, groups: &[Vec<Application>], opts: &fraig::Options) -> Option<(Vec<(Application, Application)>, Lrat)> {
358    let mut apart: HashSet<(Application, Application)> = HashSet::new();
359    loop {
360        let mut candidates: Vec<(Application, Application)> = Vec::new();
361        for group in groups {
362            let mut kept: Vec<Application> = Vec::new();
363            for &app in group {
364                match kept.iter().find(|&&k| !apart.contains(&(k, app))) {
365                    Some(&k) => candidates.push((k, app)),
366                    None => kept.push(app),
367                }
368            }
369        }
370        if candidates.is_empty() {
371            return None;
372        }
373        let pairs: Vec<(AigLit, AigLit)> =
374            candidates.iter().flat_map(|(k, a)| m.operands[k].iter().copied().zip(m.operands[a].iter().copied())).collect();
375        let (verdicts, proof, _) = fraig::decide(&m.aig, &pairs, opts);
376        let mut proven: Vec<(Application, Application)> = Vec::new();
377        let mut at = 0;
378        for &(k, a) in &candidates {
379            let width = m.operands[&k].len();
380            if verdicts[at..at + width].iter().all(|v| *v == Verdict::Equal) {
381                proven.push((k, a));
382            } else {
383                apart.insert((k, a));
384            }
385            at += width;
386        }
387        if !proven.is_empty() {
388            return Some((proven, proof));
389        }
390    }
391}
392
393/// [`decide`] over sides unrolled as each says: the claims of one name, one on each side, are an output pair.
394pub fn decide_unrolled(left: &Unrolled, right: &Unrolled, functions: &[(String, String)], opts: &fraig::Options) -> Result<Report, MiterError> {
395    let sides = [left, right];
396    let classes = Classes::new(sides, functions);
397    let mut subst: HashMap<String, String> = HashMap::new();
398    let mut made: HashSet<Application> = HashSet::new();
399    let mut rounds: Vec<Round> = Vec::new();
400    loop {
401        let m = build(sides, &subst)?;
402        let groups = groups(sides, &classes, &m, &made, opts);
403        let Some((proven, proof)) = identifications(&m, &groups, opts) else {
404            let pairs: Vec<(AigLit, AigLit)> = m.outputs.iter().map(|(_, l, r)| (*l, *r)).collect();
405            let (verdicts, proof, stats) = fraig::decide(&m.aig, &pairs, opts);
406            let verdicts = m
407                .outputs
408                .iter()
409                .zip(verdicts)
410                .map(|((name, _, _), v)| {
411                    let v = match v {
412                        Verdict::Differ { .. } => Verdict::Unknown("the outputs differ over the units as functions nothing is known of".into()),
413                        v => v,
414                    };
415                    (name.clone(), v)
416                })
417                .collect();
418            rounds.push(Round { substituted: Vec::new(), proof });
419            return Ok(Report { verdicts, rounds, stats });
420        };
421        for &(k, a) in &proven {
422            let fresh = identify(sides, &mut subst, k, a);
423            debug_assert!(fresh, "an application of a group is made one once");
424            made.insert(a);
425        }
426        rounds.push(Round { substituted: proven, proof });
427    }
428}
429
430impl Report {
431    /// Every round's derivation replays from its miter's formula and proves the operand equalities of every
432    /// identification it makes, each of two applications of one function (one name on a side, or a pair `functions`
433    /// declares), each made one once and never with itself; the last round's proves the outputs claimed equal; and no
434    /// output is claimed different.
435    pub fn check(&self, left: &Side, right: &Side, functions: &[(String, String)]) -> bool {
436        let (lc, rc) = (aligned_claims(left), aligned_claims(right));
437        let l = Unrolled { aig: left.aig, frames: left.latency + 1, bind: &aligned_bind, claims: &lc, units: left.units };
438        let r = Unrolled { aig: right.aig, frames: right.latency + 1, bind: &aligned_bind, claims: &rc, units: right.units };
439        self.check_unrolled(&l, &r, functions)
440    }
441
442    /// [`Report::check`] over sides unrolled as each says.
443    pub fn check_unrolled(&self, left: &Unrolled, right: &Unrolled, functions: &[(String, String)]) -> bool {
444        let sides = [left, right];
445        let classes = Classes::new(sides, functions);
446        let mut subst: HashMap<String, String> = HashMap::new();
447        let Some((last, earlier)) = self.rounds.split_last() else { return false };
448        for round in earlier {
449            let Ok(m) = build(sides, &subst) else { return false };
450            let mut claims = Vec::new();
451            for &(k, a) in &round.substituted {
452                let (Some(ko), Some(ao)) = (m.operands.get(&k), m.operands.get(&a)) else { return false };
453                if ko.len() != ao.len() || !classes.one(sides, k, a) {
454                    return false;
455                }
456                for (&x, &y) in ko.iter().zip(ao) {
457                    if x != y {
458                        claims.extend(fraig::claim(x, y));
459                    }
460                }
461            }
462            if !lrat::check_derivation(m.aig.num_nodes(), &fraig::formula(&m.aig), &round.proof, &claims) {
463                return false;
464            }
465            for &(k, a) in &round.substituted {
466                if !identify(sides, &mut subst, k, a) {
467                    return false;
468                }
469            }
470        }
471        let Ok(m) = build(sides, &subst) else { return false };
472        if self.verdicts.len() != m.outputs.len() {
473            return false;
474        }
475        let mut claims = Vec::new();
476        for ((name, a, b), (vname, v)) in m.outputs.iter().zip(&self.verdicts) {
477            if name != vname || matches!(v, Verdict::Differ { .. }) {
478                return false;
479            }
480            if *v == Verdict::Equal && a != b {
481                claims.extend(fraig::claim(*a, *b));
482            }
483        }
484        last.substituted.is_empty() && lrat::check_derivation(m.aig.num_nodes(), &fraig::formula(&m.aig), &last.proof, &claims)
485    }
486}
487
488#[cfg(test)]
489#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/units/tests.rs"]
490mod tests;