Skip to main content

vj_sim/constraints/
bdd.rs

1//! Bit-blasting of the constraint IR into reduced ordered BDDs (plan C3).
2//!
3//! Every [`Expr`] node becomes a vector of BDDs, one per result bit, least
4//! significant first; the whole constraint set becomes one BDD over the
5//! random variables' bits whose models are exactly the legal assignments.
6//! The gadgets (ripple-carry adder, comparator chain, barrel shifter,
7//! shift-add multiplier, restoring divider, ...) live in [`super::blast`],
8//! generic over the gate type so the SAT fallback ([`super::sat`]) shares
9//! them; this module is the BDD instance of them plus the variable layout.
10//! The unit tests check every gadget against the IR's own direct evaluator
11//! ([`super::ir::eval`]) on exhaustive or dense assignment sets.
12//!
13//! # Variable order
14//!
15//! BDD variable order is *interleaved by bit position* (bit 0 of every random
16//! variable, then bit 1, ...). Adders and comparators over two or more
17//! variables are linear in that order and exponential in the
18//! "all of x, then all of y" order. This order is internal and unrelated to
19//! the LRM solve order, which the sampler (`solve`) applies independently
20//! by choosing variables in any order it likes.
21//!
22//! # Definedness (division by zero)
23//!
24//! [`ir::eval`](super::ir::eval) reports `x / 0` and `x % 0` as an error, and
25//! `&&`, `||`, `->` and `?:` only evaluate the operand they need. The blaster
26//! mirrors that with a *definedness* BDD carried beside every value: `/` and
27//! `%` contribute "divisor is non-zero", short-circuit operators gate the
28//! right operand's definedness by the left operand's value, and the final
29//! conjunct is `value & defined`. So an assignment on which `eval` would
30//! return `DivideByZero` is not a model; it is "not legal", the same way the
31//! LRM treats a constraint expression it cannot evaluate as unsatisfied.
32//!
33//! # Node budget
34//!
35//! Some functions have no small BDD in any variable order; the multiplier
36//! is the canonical one (Bryant 1991). Every apply is run with a node limit
37//! ([`NODE_BUDGET`], or the one passed to [`compile`]); exceeding it is the
38//! typed [`BddBuildError::TooLarge`], never a hang or an out-of-memory. That
39//! error is the trigger of the SAT fallback ([`super::sat`]), which
40//! [`super::solve::Engine`] switches to.
41
42use super::blast::{Blaster, Gates};
43use super::ir::{ConstraintEvalError, Expr, IrError, StateId, Ty, VarId};
44use biodivine_lib_bdd::{Bdd, BddValuation, BddVariable, BddVariableSet, op_function};
45use core::fmt;
46use num_bigint::BigUint;
47use num_traits::Zero;
48
49/// The default per-BDD node limit.
50///
51/// Calibrated by `tests/constraint_bdd.rs::calibrate_node_budget` (an
52/// `#[ignore]`d benchmark: `cargo test -p vj-sim --release --test
53/// constraint_bdd -- --ignored --nocapture`). Measured results, `--release`,
54/// the final BDD size of the whole formula and the time to build or to give
55/// up (the formula is `x * y == z` over three `w`-bit variables unless
56/// noted):
57///
58/// | formula              | outcome at 200k budget | final nodes | seconds |
59/// |----------------------|------------------------|-------------|---------|
60/// | `x + y == z`, 64-bit | builds                 | 572         | 0.00    |
61/// | `x < y`, 64-bit      | builds                 | 193         | 0.00    |
62/// | `x / y == z`, 10-bit | builds                 | 8,160       | 0.11    |
63/// | multiplier, w = 8    | builds                 | 2,692       | 0.00    |
64/// | multiplier, w = 10   | builds                 | 14,456      | 0.01    |
65/// | multiplier, w = 12   | builds                 | 77,179      | 0.06    |
66/// | multiplier, w = 13   | builds                 | 181,226     | 0.17    |
67/// | multiplier, w = 14   | `TooLarge` (fits 500k) | 412,469 at 500k | 0.45 |
68/// | multiplier, w = 16   | `TooLarge`             | -           | 0.45    |
69/// | multiplier, w = 32   | `TooLarge`             | -           | 0.88    |
70/// | multiplier, w = 64   | `TooLarge`             | -           | 2.05    |
71///
72/// The multiplier's BDD grows by about 2.3x per extra bit of width, so the
73/// knee is sharp: 200,000 keeps every adder, comparator, shifter and
74/// divider up to the 64-bit machine widths (all under 10,000 nodes in the
75/// runs above) plus a multiplier up to 13 bits on the exact path, and any
76/// formula that is hopeless for BDDs gives up in about a second. Raising it to
77/// 2,000,000 still cannot build a 16-bit multiplier (it fails after 4.9 s)
78/// while a failing 32-bit one then takes 28 s and a 64-bit one 47 s to be
79/// reported; lowering it to 100,000 loses the 13-bit multiplier for only a
80/// 2x saving on the failure path. Each node is 16 bytes, so one BDD is
81/// bounded to about 3 MiB.
82pub const NODE_BUDGET: usize = 200_000;
83
84/// The largest number of BDD variables (the library's own cap).
85pub const MAX_BDD_VARS: usize = u16::MAX as usize - 2;
86
87/// Why a constraint set could not be turned into a BDD.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum BddBuildError {
90    /// A BDD operation would exceed the node budget; the engine falls back
91    /// to SAT.
92    TooLarge {
93        /// The budget that was exceeded.
94        budget: usize,
95        /// The IR operation being blasted when it was exceeded.
96        during: &'static str,
97    },
98    /// The CNF the SAT fallback would build exceeds its clause limit.
99    CnfTooLarge {
100        /// The clause limit that was exceeded.
101        limit: usize,
102        /// The IR operation being blasted when it was exceeded.
103        during: &'static str,
104    },
105    /// More random-variable bits than the BDD library has variables.
106    TooManyBits {
107        /// Total bits requested.
108        bits: usize,
109    },
110    /// The IR is ill-typed (a builder bug).
111    Ir(IrError),
112    /// A state variable read has an X/Z bit (LRM 18.3, decision E9).
113    StateUnknown(StateId),
114    /// The IR names a state variable the caller did not supply.
115    UnboundState(StateId),
116    /// The IR names a random variable the caller did not declare.
117    UnboundVar(VarId),
118    /// The IR still holds an array-level node (a run-time expansion bug).
119    Unexpanded,
120    /// The IR uses a random variable at a width other than its declaration.
121    VarWidthMismatch {
122        /// The variable.
123        var: VarId,
124        /// Its declared width.
125        declared: u32,
126        /// The width the expression reads it at.
127        used: u32,
128    },
129}
130
131impl fmt::Display for BddBuildError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            BddBuildError::TooLarge { budget, during } => write!(
135                f,
136                "constraint BDD exceeded the {budget}-node budget while building `{during}`"
137            ),
138            BddBuildError::CnfTooLarge { limit, during } => write!(
139                f,
140                "constraint CNF exceeded the {limit}-clause limit while building `{during}`"
141            ),
142            BddBuildError::TooManyBits { bits } => write!(
143                f,
144                "{bits} random-variable bits exceed the BDD backend's {MAX_BDD_VARS}-variable limit"
145            ),
146            BddBuildError::Ir(e) => fmt::Display::fmt(e, f),
147            BddBuildError::StateUnknown(s) => {
148                fmt::Display::fmt(&ConstraintEvalError::StateUnknown(*s), f)
149            }
150            BddBuildError::UnboundState(s) => {
151                fmt::Display::fmt(&ConstraintEvalError::UnboundState(*s), f)
152            }
153            BddBuildError::UnboundVar(v) => {
154                fmt::Display::fmt(&ConstraintEvalError::UnboundVar(*v), f)
155            }
156            BddBuildError::Unexpanded => fmt::Display::fmt(&ConstraintEvalError::Unexpanded, f),
157            BddBuildError::VarWidthMismatch {
158                var,
159                declared,
160                used,
161            } => write!(
162                f,
163                "random variable #{var} is declared {declared} bits wide but read as {used}"
164            ),
165        }
166    }
167}
168
169impl std::error::Error for BddBuildError {}
170
171impl From<IrError> for BddBuildError {
172    fn from(e: IrError) -> Self {
173        BddBuildError::Ir(e)
174    }
175}
176
177/// Where each random variable's bits live in the BDD.
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub struct Layout {
180    /// Declared type of each variable, indexed by [`VarId`].
181    pub tys: Vec<Ty>,
182    /// `bits[v][i]` is the BDD variable for bit `i` (LSB = 0) of variable `v`.
183    pub bits: Vec<Vec<BddVariable>>,
184}
185
186impl Layout {
187    /// Interleaved layout (module docs) for variables of the given types.
188    pub fn interleaved(tys: &[Ty]) -> Result<Layout, BddBuildError> {
189        let total: usize = tys.iter().map(|t| t.width as usize).sum();
190        if total > MAX_BDD_VARS {
191            return Err(BddBuildError::TooManyBits { bits: total });
192        }
193        let max_w = tys.iter().map(|t| t.width).max().unwrap_or(0);
194        let mut bits: Vec<Vec<BddVariable>> = tys
195            .iter()
196            .map(|t| Vec::with_capacity(t.width as usize))
197            .collect();
198        let mut next = 0usize;
199        for pos in 0..max_w {
200            for (v, t) in tys.iter().enumerate() {
201                if t.width > pos {
202                    bits[v].push(BddVariable::from_index(next));
203                    next += 1;
204                }
205            }
206        }
207        Ok(Layout {
208            tys: tys.to_vec(),
209            bits,
210        })
211    }
212
213    /// Total number of BDD variables.
214    pub fn num_bits(&self) -> usize {
215        self.tys.iter().map(|t| t.width as usize).sum()
216    }
217
218    /// The BDD valuation that encodes `values` (one per variable).
219    ///
220    /// # Panics
221    /// If `values` does not have one entry per declared variable.
222    pub fn encode(&self, values: &[BigUint]) -> BddValuation {
223        assert_eq!(values.len(), self.tys.len(), "one value per variable");
224        let n = u16::try_from(self.num_bits()).expect("checked by interleaved()");
225        let mut val = BddValuation::all_false(n);
226        for (v, vars) in self.bits.iter().enumerate() {
227            for (i, bv) in vars.iter().enumerate() {
228                val.set_value(*bv, values[v].bit(i as u64));
229            }
230        }
231        val
232    }
233
234    /// The variable values a BDD valuation encodes.
235    pub fn decode(&self, val: &BddValuation) -> Vec<BigUint> {
236        self.bits
237            .iter()
238            .map(|vars| {
239                let mut x = BigUint::zero();
240                for (i, bv) in vars.iter().enumerate() {
241                    if val.value(*bv) {
242                        x.set_bit(i as u64, true);
243                    }
244                }
245                x
246            })
247            .collect()
248    }
249}
250
251/// The result of [`compile`]: one BDD whose models are the legal assignments.
252#[derive(Clone, Debug)]
253pub struct Compiled {
254    /// The conjunction of every constraint (and definedness side condition).
255    pub bdd: Bdd,
256    /// Variable-to-BDD-variable mapping.
257    pub layout: Layout,
258}
259
260impl Compiled {
261    /// Whether the assignment `values` is a model.
262    pub fn accepts(&self, values: &[BigUint]) -> bool {
263        self.bdd.eval_in(&self.layout.encode(values))
264    }
265}
266
267/// Bit-blasts the conjunction of `conjuncts` over variables of types
268/// `var_tys` (indexed by [`VarId`]) with state variables fixed to `states`
269/// (a `None` entry is an X/Z-holding state and fails if read). Every BDD
270/// operation is limited to `budget` nodes.
271pub fn compile(
272    var_tys: &[Ty],
273    states: &[Option<BigUint>],
274    conjuncts: &[Expr],
275    budget: usize,
276) -> Result<Compiled, BddBuildError> {
277    let layout = Layout::interleaved(var_tys)?;
278    let n = u16::try_from(layout.num_bits()).expect("checked by interleaved()");
279    let gates = BddGates {
280        set: BddVariableSet::new_anonymous(n),
281        layout: &layout,
282        budget,
283    };
284    let mut acc = gates.set.mk_true();
285    {
286        let widths: Vec<usize> = layout.bits.iter().map(Vec::len).collect();
287        let b = Blaster {
288            g: &gates,
289            widths: &widths,
290            states,
291        };
292        for c in conjuncts {
293            c.validate()?;
294            let t = b.conjunct(c)?;
295            acc = gates.and("constraint conjunction", &acc, &t)?;
296        }
297    }
298    Ok(Compiled { bdd: acc, layout })
299}
300
301type OpFn = fn(Option<bool>, Option<bool>) -> Option<bool>;
302
303/// The BDD back end of the shared bit-blaster ([`super::blast`]): a bit is a
304/// [`Bdd`], and every binary operation is limited to `budget` nodes.
305struct BddGates<'a> {
306    set: BddVariableSet,
307    layout: &'a Layout,
308    budget: usize,
309}
310
311impl BddGates<'_> {
312    fn lim(&self, during: &'static str, f: OpFn, a: &Bdd, b: &Bdd) -> Result<Bdd, BddBuildError> {
313        Bdd::binary_op_with_limit(self.budget, a, b, f).ok_or(BddBuildError::TooLarge {
314            budget: self.budget,
315            during,
316        })
317    }
318}
319
320impl Gates for BddGates<'_> {
321    type Bit = Bdd;
322
323    fn constant(&self, v: bool) -> Bdd {
324        if v {
325            self.set.mk_true()
326        } else {
327            self.set.mk_false()
328        }
329    }
330
331    fn as_const(&self, a: &Bdd) -> Option<bool> {
332        if a.is_true() {
333            Some(true)
334        } else if a.is_false() {
335            Some(false)
336        } else {
337            None
338        }
339    }
340
341    fn input(&self, var: VarId, i: usize) -> Bdd {
342        self.set.mk_var(self.layout.bits[var as usize][i])
343    }
344
345    fn not(&self, a: &Bdd) -> Bdd {
346        a.not()
347    }
348
349    fn and(&self, d: &'static str, a: &Bdd, b: &Bdd) -> Result<Bdd, BddBuildError> {
350        if a.is_false() || b.is_false() {
351            return Ok(self.set.mk_false());
352        }
353        if a.is_true() {
354            return Ok(b.clone());
355        }
356        if b.is_true() || a == b {
357            return Ok(a.clone());
358        }
359        self.lim(d, op_function::and, a, b)
360    }
361
362    fn or(&self, d: &'static str, a: &Bdd, b: &Bdd) -> Result<Bdd, BddBuildError> {
363        if a.is_true() || b.is_true() {
364            return Ok(self.set.mk_true());
365        }
366        if a.is_false() {
367            return Ok(b.clone());
368        }
369        if b.is_false() || a == b {
370            return Ok(a.clone());
371        }
372        self.lim(d, op_function::or, a, b)
373    }
374
375    fn xor(&self, d: &'static str, a: &Bdd, b: &Bdd) -> Result<Bdd, BddBuildError> {
376        if a.is_false() {
377            return Ok(b.clone());
378        }
379        if b.is_false() {
380            return Ok(a.clone());
381        }
382        if a.is_true() {
383            return Ok(b.not());
384        }
385        if b.is_true() {
386            return Ok(a.not());
387        }
388        if a == b {
389            return Ok(self.set.mk_false());
390        }
391        self.lim(d, op_function::xor, a, b)
392    }
393
394    fn mux(&self, d: &'static str, s: &Bdd, a: &Bdd, b: &Bdd) -> Result<Bdd, BddBuildError> {
395        if s.is_true() {
396            return Ok(a.clone());
397        }
398        if s.is_false() {
399            return Ok(b.clone());
400        }
401        if a == b {
402            return Ok(a.clone());
403        }
404        let t = self.and(d, s, a)?;
405        let e = self.and(d, &s.not(), b)?;
406        self.or(d, &t, &e)
407    }
408}