sv_sim_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 sv-sim-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}