Skip to main content

logicaffeine_proof/
streams.rs

1//! Stream equivalence over units proven apart: two circuits that take a stream of tokens through a valid-ready handshake
2//! and give a stream back, each at its own width and its own pace, decided equal token for token under an arrival
3//! pattern.
4//!
5//! The environment ([`Environment`]) offers a token at the clocks it says, takes one at the clocks it says, and holds
6//! every other input as it says (a reset's level, a value both sides share for the whole run). Under it each side's
7//! handshakes are a schedule ([`schedule`]): the clocks it takes its input tokens at and gives its output tokens at, each
8//! proven the constant it is with the data free, so a side whose handshake depends on the data has no schedule and is
9//! refused by name. The sides are then unrolled ([`crate::units::Unrolled`]) with each side's j-th input token read where
10//! that side takes it ([`Ports::token`] names the symbols a token's bits are, so a side two words wide takes words 2j and
11//! 2j+1), and each side's k-th output token claimed where that side gives it, its bits `out<k>.<i>`; [`decide`] decides
12//! the claims over the units as [`crate::units::decide_unrolled`] does. A token one side gives past what the other gives
13//! in the clocks unrolled is counted, not claimed.
14
15use std::collections::HashSet;
16use std::fmt;
17
18use crate::aig::{Aig, AigLit, Init, Leaf};
19use crate::fraig;
20use crate::lrat::{self, Lrat};
21use crate::sweep::{MiterError, Verdict};
22use crate::units::{self, Binding, Claim, Unit, Unrolled};
23
24/// A side's stream ports, by bit name: the input stream's valid, ready and data, the output stream's valid, ready (none
25/// for a side that never waits for it) and data; and what bit `bit` of the side's j-th input token reads (`token(j,
26/// bit)`, a symbol the other side's token reads too).
27pub struct Ports<'a> {
28    pub in_valid: String,
29    pub in_ready: String,
30    pub in_data: Vec<String>,
31    pub out_valid: String,
32    pub out_ready: Option<String>,
33    pub out_data: Vec<String>,
34    pub token: &'a dyn Fn(usize, &str) -> Binding,
35}
36
37/// The arrival pattern a side runs under, `frames` clocks: whether a token is offered at each clock, whether a given one
38/// is taken (or, for a side that never waits, watched for) at each clock, and what every other input is at each clock.
39/// Only the handshakes at those clocks are the side's schedule, so the clocks before a reset settles are left out by
40/// offering and taking nothing there.
41pub struct Environment<'a> {
42    pub frames: usize,
43    pub offer: &'a dyn Fn(usize) -> bool,
44    pub accept: &'a dyn Fn(usize) -> bool,
45    pub other: &'a dyn Fn(&str, usize) -> Binding,
46}
47
48/// One side: its cut circuit, its units, its ports.
49pub struct StreamSide<'a> {
50    pub aig: &'a Aig,
51    pub units: &'a [Unit],
52    pub ports: Ports<'a>,
53}
54
55/// A side's schedule under its environment: the clocks it took its input tokens at and gave its output tokens at, and
56/// the derivation, from the side unrolled alone, that every handshake is the constant it was taken to be.
57#[derive(Clone, Debug)]
58pub struct Schedule {
59    pub took: Vec<usize>,
60    pub gave: Vec<usize>,
61    pub proof: Lrat,
62}
63
64/// Why two streams cannot be decided.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum StreamError {
67    Miter(MiterError),
68    /// A port the side's circuit has no bit of.
69    Port(String),
70    /// A handshake that is not one constant at a clock whatever the data: the side has no schedule.
71    DataDependent { port: String, frame: usize },
72    /// A handshake the search could not prove constant within its budget.
73    Undecided { port: String, frame: usize },
74}
75
76impl fmt::Display for StreamError {
77    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78        match self {
79            StreamError::Miter(e) => write!(f, "{e:?}"),
80            StreamError::Port(p) => write!(f, "no port {p}"),
81            StreamError::DataDependent { port, frame } => write!(f, "{port} at clock {frame} depends on the data: the side has no schedule"),
82            StreamError::Undecided { port, frame } => write!(f, "{port} at clock {frame} was not proven constant"),
83        }
84    }
85}
86
87/// The side unrolled alone under its environment, its input data and every unit's result free at every clock, and its
88/// handshakes at every clock: the input stream's ready, the output stream's valid. Only what the handshakes read
89/// through any number of clocks is unrolled ([`Aig::sequential_cone`]): a datapath they never read costs nothing.
90struct Alone {
91    aig: Aig,
92    ready: Vec<AigLit>,
93    valid: Vec<AigLit>,
94}
95
96fn alone(side: &StreamSide, env: &Environment) -> Result<Alone, StreamError> {
97    let p = &side.ports;
98    for port in [&p.in_valid].into_iter().chain(&p.in_data).chain(p.out_ready.iter()) {
99        side.aig.input_index(port).ok_or_else(|| StreamError::Port(port.clone()))?;
100    }
101    for port in [&p.in_ready, &p.out_valid].into_iter().chain(&p.out_data) {
102        side.aig.output_index(port).ok_or_else(|| StreamError::Port(port.clone()))?;
103    }
104    let handshakes = [side.aig.output_index(&p.in_ready).expect("checked"), side.aig.output_index(&p.out_valid).expect("checked")];
105    let cone = side.aig.sequential_cone(&handshakes);
106    let g = &cone;
107    let results: HashSet<&str> = side.units.iter().flat_map(|u| u.results.iter().map(String::as_str)).collect();
108    let data: HashSet<&str> = p.in_data.iter().map(String::as_str).collect();
109    let mut m = Aig::new();
110    let mut shared: std::collections::HashMap<String, AigLit> = std::collections::HashMap::new();
111    let mut state: Vec<AigLit> = g
112        .latches()
113        .iter()
114        .map(|l| match l.init {
115            Init::Zero => AigLit::FALSE,
116            Init::One => AigLit::TRUE,
117            Init::Free => m.add_input(None),
118        })
119        .collect();
120    let (mut ready, mut valid) = (Vec::new(), Vec::new());
121    let (ri, vi) = (0, 1);
122    for k in 0..env.frames {
123        let frame_inputs: Vec<AigLit> = g
124            .inputs()
125            .iter()
126            .map(|port| {
127                let Some(name) = port.name.as_deref() else { return m.add_input(None) };
128                let constant = |b: bool| if b { AigLit::TRUE } else { AigLit::FALSE };
129                if name == p.in_valid {
130                    constant((env.offer)(k))
131                } else if p.out_ready.as_deref() == Some(name) {
132                    constant((env.accept)(k))
133                } else if data.contains(name) || results.contains(name) {
134                    m.add_input(None)
135                } else {
136                    match (env.other)(name, k) {
137                        Binding::Const(b) => constant(b),
138                        Binding::Shared(symbol) => *shared.entry(symbol.clone()).or_insert_with(|| m.add_input(Some(symbol))),
139                        Binding::Fresh => m.add_input(None),
140                    }
141                }
142            })
143            .collect();
144        let map = m.import(g, &mut |leaf| match leaf {
145            Leaf::Input(i) => frame_inputs[i],
146            Leaf::Latch(j) => state[j],
147        });
148        let at = |l: AigLit| map[l.node() as usize].with_phase(l.is_complemented());
149        ready.push(at(g.outputs()[ri].lit));
150        valid.push(at(g.outputs()[vi].lit));
151        state = g.latches().iter().map(|l| at(l.next)).collect();
152    }
153    Ok(Alone { aig: m, ready, valid })
154}
155
156/// The constant each handshake is taken to be, from random simulation of the side alone: `None` when the simulation
157/// already shows it moving with the data.
158fn guesses(a: &Alone, seed: u64) -> Vec<Option<bool>> {
159    let words = 8;
160    let mut state = seed | 1;
161    let mut next = move || {
162        state ^= state << 13;
163        state ^= state >> 7;
164        state ^= state << 17;
165        state
166    };
167    let inputs: Vec<u64> = (0..a.aig.inputs().len() * words).map(|_| next()).collect();
168    let values = a.aig.simulate(words, &inputs, &[]);
169    a.ready
170        .iter()
171        .chain(&a.valid)
172        .map(|&l| {
173            let word = |w: usize| Aig::word(&values, words, l, w);
174            match (0..words).map(word).collect::<Vec<_>>() {
175                v if v.iter().all(|&x| x == 0) => Some(false),
176                v if v.iter().all(|&x| x == !0) => Some(true),
177                _ => None,
178            }
179        })
180        .collect()
181}
182
183/// The handshakes paired with the constants they are taken to be.
184fn handshake_pairs(a: &Alone, guessed: &[bool]) -> Vec<(AigLit, AigLit)> {
185    a.ready.iter().chain(&a.valid).zip(guessed).map(|(&l, &b)| (l, if b { AigLit::TRUE } else { AigLit::FALSE })).collect()
186}
187
188/// The handshakes a schedule rests on, as indices into `ready` then `valid`: the input's ready where a token is
189/// offered, the output's valid where one is taken.
190fn relevant(env: &Environment, n: usize) -> Vec<usize> {
191    (0..n).filter(|&k| (env.offer)(k)).chain((0..n).filter(|&k| (env.accept)(k)).map(|k| n + k)).collect()
192}
193
194/// A side's schedule under its environment, every handshake it rests on proven the constant it is.
195pub fn schedule(side: &StreamSide, env: &Environment, opts: &fraig::Options) -> Result<Schedule, StreamError> {
196    let a = alone(side, env)?;
197    let n = a.ready.len();
198    let name = |i: usize| if i < n { (side.ports.in_ready.clone(), i) } else { (side.ports.out_valid.clone(), i - n) };
199    let all = guesses(&a, opts.seed);
200    let mut guessed = vec![false; 2 * n];
201    let used = relevant(env, n);
202    for &i in &used {
203        guessed[i] = all[i].ok_or_else(|| {
204            let (port, frame) = name(i);
205            StreamError::DataDependent { port, frame }
206        })?;
207    }
208    let pairs = handshake_pairs(&a, &guessed);
209    let asked: Vec<(AigLit, AigLit)> = used.iter().map(|&i| pairs[i]).collect();
210    let (verdicts, proof, _) = fraig::decide(&a.aig, &asked, opts);
211    for (&i, v) in used.iter().zip(&verdicts) {
212        let (port, frame) = name(i);
213        match v {
214            Verdict::Equal => {}
215            Verdict::Differ { .. } => return Err(StreamError::DataDependent { port, frame }),
216            Verdict::Unknown(_) => return Err(StreamError::Undecided { port, frame }),
217        }
218    }
219    let took = (0..n).filter(|&k| (env.offer)(k) && guessed[k]).collect();
220    let gave = (0..n).filter(|&k| (env.accept)(k) && guessed[n + k]).collect();
221    Ok(Schedule { took, gave, proof })
222}
223
224impl Schedule {
225    /// The clocks are in the order the tokens pass (token j the j-th taken, the j-th given), and the derivation replays
226    /// from the side unrolled alone and proves every handshake the constant the schedule says.
227    pub fn check(&self, side: &StreamSide, env: &Environment) -> bool {
228        if !self.took.windows(2).all(|w| w[0] < w[1]) || !self.gave.windows(2).all(|w| w[0] < w[1]) {
229            return false;
230        }
231        let Ok(a) = alone(side, env) else { return false };
232        let n = a.ready.len();
233        let took: HashSet<usize> = self.took.iter().copied().collect();
234        let gave: HashSet<usize> = self.gave.iter().copied().collect();
235        // The constants the schedule says, where it rests on them: taken where a token was offered, given where one was
236        // taken.
237        let guessed: Vec<bool> = (0..n).map(|k| (env.offer)(k) && took.contains(&k)).chain((0..n).map(|k| (env.accept)(k) && gave.contains(&k))).collect();
238        if self.took.iter().any(|&k| k >= n || !(env.offer)(k)) || self.gave.iter().any(|&k| k >= n || !(env.accept)(k)) {
239            return false;
240        }
241        let known = relevant(env, n);
242        let pairs = handshake_pairs(&a, &guessed);
243        let claims: Vec<Vec<crate::cdcl::Lit>> =
244            known.iter().filter(|&&i| pairs[i].0 != pairs[i].1).flat_map(|&i| fraig::claim(pairs[i].0, pairs[i].1)).collect();
245        lrat::check_derivation(a.aig.num_nodes(), &fraig::formula(&a.aig), &self.proof, &claims)
246    }
247}
248
249/// A decision of two streams: a verdict per claimed output bit (`out<k>.<i>`), each side's schedule, the units' rounds,
250/// and how many tokens each side gave in the clocks unrolled.
251#[derive(Clone, Debug)]
252pub struct Report {
253    pub verdicts: Vec<(String, Verdict)>,
254    pub schedules: [Schedule; 2],
255    pub units: units::Report,
256    pub gave: [usize; 2],
257}
258
259impl Report {
260    pub fn all_equal(&self) -> bool {
261        self.verdicts.iter().all(|(_, v)| *v == Verdict::Equal)
262    }
263}
264
265/// What a side's input is at a clock once its schedule is known: the environment's handshake, the token it takes there,
266/// a fresh value where it takes none, every other input as the environment holds it.
267fn bound<'a>(side: &'a StreamSide<'a>, env: &'a Environment<'a>, took: &'a [usize]) -> impl Fn(&str, usize) -> Binding + 'a {
268    let data: HashSet<String> = side.ports.in_data.iter().cloned().collect();
269    move |name: &str, k: usize| {
270        if name == side.ports.in_valid {
271            Binding::Const((env.offer)(k))
272        } else if side.ports.out_ready.as_deref() == Some(name) {
273            Binding::Const((env.accept)(k))
274        } else if data.contains(name) {
275            match took.iter().position(|&t| t == k) {
276                Some(j) => (side.ports.token)(j, name),
277                None => Binding::Fresh,
278            }
279        } else {
280            (env.other)(name, k)
281        }
282    }
283}
284
285/// The output bits a side claims: its k-th given token's bits, for every token both sides give.
286fn claims(side: &StreamSide, gave: &[usize], tokens: usize) -> Vec<Claim> {
287    gave[..tokens]
288        .iter()
289        .enumerate()
290        .flat_map(|(k, &frame)| side.ports.out_data.iter().enumerate().map(move |(i, bit)| Claim { name: format!("out{k}.{i}"), output: bit.clone(), frame }))
291        .collect()
292}
293
294/// Decide `left` against `right`, each under its environment, over their units, the functions of `functions` (each a
295/// left function, the right one proven equal to it) the only ones identified.
296pub fn decide(
297    left: &StreamSide,
298    left_env: &Environment,
299    right: &StreamSide,
300    right_env: &Environment,
301    functions: &[(String, String)],
302    opts: &fraig::Options,
303) -> Result<Report, StreamError> {
304    if left.ports.out_data.len() != right.ports.out_data.len() {
305        return Err(StreamError::Port(format!("{} output bits against {}", left.ports.out_data.len(), right.ports.out_data.len())));
306    }
307    let (ls, rs) = (schedule(left, left_env, opts)?, schedule(right, right_env, opts)?);
308    let tokens = ls.gave.len().min(rs.gave.len());
309    let (lc, rc) = (claims(left, &ls.gave, tokens), claims(right, &rs.gave, tokens));
310    let (lt, rt) = (ls.took.clone(), rs.took.clone());
311    let (lb, rb) = (bound(left, left_env, &lt), bound(right, right_env, &rt));
312    let l = Unrolled { aig: left.aig, frames: left_env.frames, bind: &lb, claims: &lc, units: left.units };
313    let r = Unrolled { aig: right.aig, frames: right_env.frames, bind: &rb, claims: &rc, units: right.units };
314    let report = units::decide_unrolled(&l, &r, functions, opts).map_err(StreamError::Miter)?;
315    let gave = [ls.gave.len(), rs.gave.len()];
316    Ok(Report { verdicts: report.verdicts.clone(), schedules: [ls, rs], units: report, gave })
317}
318
319impl Report {
320    /// Both schedules' derivations replay and the units' rounds check over the sides bound as the schedules say.
321    pub fn check(&self, left: &StreamSide, left_env: &Environment, right: &StreamSide, right_env: &Environment, functions: &[(String, String)]) -> bool {
322        let [ls, rs] = &self.schedules;
323        if !ls.check(left, left_env) || !rs.check(right, right_env) {
324            return false;
325        }
326        let tokens = ls.gave.len().min(rs.gave.len());
327        if ls.gave.len() > left_env.frames || rs.gave.len() > right_env.frames {
328            return false;
329        }
330        let (lc, rc) = (claims(left, &ls.gave, tokens), claims(right, &rs.gave, tokens));
331        let (lb, rb) = (bound(left, left_env, &ls.took), bound(right, right_env, &rs.took));
332        let l = Unrolled { aig: left.aig, frames: left_env.frames, bind: &lb, claims: &lc, units: left.units };
333        let r = Unrolled { aig: right.aig, frames: right_env.frames, bind: &rb, claims: &rc, units: right.units };
334        self.units.check_unrolled(&l, &r, functions)
335    }
336}
337
338#[cfg(test)]
339#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/streams/tests.rs"]
340mod tests;