A native Rust engine for Context-Free-Language Ordered Binary Decision Diagrams (CFLOBDDs): a compressed representation of boolean functions over
Like classical BDDs, CFLOBDDs let you build, combine, evaluate, and count satisfying assignments of boolean expressions. Unlike BDDs, their hierarchical structure can give exponential compression on many functions of practical interest.
The name is a phonetic spelling of CFLOBDD as you'd say it after you've stopped trying: "sea-flow". bdd is just bdd.
v0.2: unweighted boolean operations, optimized. The engine implements the full CFLOBDD pipeline (hash-cons canonical store, PairProduct, Reduce, ApplyAndReduce) for boolean-valued functions. Phase 2 optimizations have brought wall-clock performance to parity with or better than the C++ reference engine on every measured workload, with at least one workload running ≥10× faster (see Performance). Weighted variants (complex, big-float, Fourier) and parallelism are out of scope for this release.
The codebase started as a Rust port of the algorithms from trishullab/cflobdd; algorithm provenance is credited in source comments where applicable. seaflowbdd is its own implementation with its own data layout and lifecycle.
[dependencies]
seaflowbdd = "0.1"The crate is pure Rust. No C++ toolchain or system libraries required. Two small dependencies: hashbrown and smallvec.
use seaflowbdd::Manager;
// A manager fixed at level 2 represents functions over 2^2 = 4 variables.
let mut m = Manager::new(2);
let x0 = m.mk_proj(0); // x_0
let x1 = m.mk_proj(1); // x_1
let f = m.and(x0, x1); // x_0 AND x_1
// Evaluate on the assignment [x_0 = true, x_1 = true, x_2 = false, x_3 = false]:
assert!( m.evaluate(f, &[true, true, false, false]));
assert!(!m.evaluate(f, &[false, true, true, true ]));
// Count satisfying assignments. Out of 16 total assignments, exactly
// those with x_0 = x_1 = 1 satisfy f, regardless of x_2 / x_3.
assert_eq!(m.path_count(f), 4);
// Bdd equality is canonical: structurally equal Bdds compare by id.
let f_again = {
let a = m.mk_proj(0);
let b = m.mk_proj(1);
m.and(a, b)
};
assert_eq!(f, f_again);See examples/hello.rs and examples/majority.rs for complete runnable programs.
All operations are methods on &mut Manager (or &Manager for read-only ops). Bdd is a Copy value handle; the actual graph lives on the manager.
| Method | Notes |
|---|---|
Manager::new(level: u8) |
Create an engine. Total variables = |
mk_true(), mk_false(), mk_proj(i)
|
Primitives. |
and, or, xor, not, nand, nor, iff, implies
|
Pointwise boolean ops. |
apply(f, g, op: BoolOp) |
Arbitrary 2-input op via a 2x2 truth table. |
restrict(f, var, value) |
Substitute a constant for a variable. |
compose(f, var, g) |
Substitute a Bdd for a variable. |
exists(f, var), forall(f, var)
|
Variable quantification. |
evaluate(f, &[bool]) |
Evaluate on an assignment. |
path_count(f) -> i128 |
Count satisfying assignments. Lazily memoized. |
eq(f, g) |
Canonical equality (id comparison after hash-consing). |
reachable_node_count(f), reachable_edge_count(f)
|
Structural diagnostics. |
-
Hash-consed canonical store. Internal nodes are interned: structurally equal subgraphs share a single
NodeId. Equality is canonical and reduces to id comparison. -
No global state. Multiple
Managers coexist with disjoint id spaces. No singletons, nostatic mut, no thread-local nonsense. -
BddisCopy. A handle is twou32s. Passing Bdds around is free; the graph lives on the manager. - Single-threaded. No locks, no atomics on the hot path. Concurrency is out of scope for now.
-
i128 path counts. Path counting uses 128-bit integers in linear space, valid up to
$2^{128}$ paths (covers level ≤ 7). Other engines store path counts in log space usinglong double; we don't need to. - Memoization. PairProduct, Reduce, and path-count results are memoized per-manager.
See PLAN.md in the repo for the full design document, including the bounded-context analysis, aggregate boundaries, and ubiquitous-language glossary.
-
not(not(f))is semantically equal tofbut not currently canonically equal.notflips the value-map bits without re-canonicalizing their order, so the resultingBdds share a root id but may have distinct value-map ids. Compare viaevaluateor by canonicalizing through any boolean op when this matters. To be tightened in a future release. -
Levels are bounded by overflow.
path_countpanics on i128 overflow, which can happen at level ≥ 7 only if the function has near-$2^{128}$ satisfying assignments.Manager::newaccepts levels up to 30 (i.e. up to ~1 billion variables) but path counting at level ≥ 7 may overflow. - No serialization yet. Bdds are not directly persistable across processes. Build them in memory, use them, drop the manager.
Microbenchmarks comparing seaflowbdd v0.2 against the trishullab/cflobdd C++ reference engine, single-threaded on Apple Silicon (M3 Max). Negative numbers favor seaflowbdd.
Two cuts. from_scratch builds a fresh seaflowbdd Manager per iteration (cold caches every time); the C++ engine has no cache-reset API, so its caches stay warm across iterations within a benchmark. This biases toward the oracle. warm pre-builds projections outside the iter loop so both engines run on warm memo caches; this isolates the boolean-apply cost from projection construction.
| Workload | seaflowbdd | C++ oracle | ratio |
|---|---|---|---|
projection_construction_32 |
2.99 µs | 35.40 µs | 0.08× (11.8× faster) |
and_tree/32 from_scratch |
34.36 µs | 40.99 µs | 0.84× (1.2× faster) |
and_tree/8 from_scratch |
15.65 µs | 10.13 µs | 1.55× |
and_tree/4 from_scratch |
8.67 µs | 5.11 µs | 1.70× |
and_tree/32 warm |
1.52 µs | 4.77 µs | 0.32× (3.1× faster) |
or_tree/32 warm |
1.54 µs | 4.76 µs | 0.32× (3.1× faster) |
xor_tree/32 warm |
1.52 µs | 4.82 µs | 0.31× (3.2× faster) |
repeated_apply (16 vars × 100 iters) |
74.9 µs | 226.2 µs | 0.33× (3.0× faster) |
Every measured op is within 2× of the C++ engine's wall-clock, and most are several times faster.
The principal Phase 2 wins were:
- Reduce memo cache. The C++ engine has one; v0.1 didn't. Adding it turned warm-bench costs from "rebuild from scratch every call" into "lookup in a memo table". (~16× speedup on warm benches.)
- Hash-table dedup without key cloning. The node dedup table now uses hashbrown's
HashTablewith hash + eq closures that probe directly against existing records, instead of aHashMap<NodeRecord, NodeId>that cloned the key on every insert. - Precomputed common return maps. The maps
[0],[1],[0, 1]show up everywhere in construction; preinterning atManager::newremoves a HashMap lookup per use. - Stack-allocated SmallVec snapshots instead of
to_vec()heap allocations on hot recursion paths in PairProduct, Reduce, and Restrict. - FxHash for memo cache keys.
(NodeId, NodeId)and(NodeId, ReturnMapId)are u64-equivalent; FxHash beats a randomized hasher on these. - Custom
Hashimpl forNodeRecordthat hashes packedu64connection fields directly, instead of the derived per-field hashing.
Possible directions, in no particular order:
- Tighten
notto canonicalize the value-map ordering. - Garbage collection. Without refcount, the canonical store grows monotonically. A mark-and-compact GC keyed on user-supplied roots is a natural fit and is sketched in
PLAN.md. - Specialized fast paths for small return-map sizes, common shape pairs (1×1, 1×2, 2×2), and SBO of B-connections inline in NodeRecord.
- Weighted variants (complex, big-float, Fourier) for quantum-circuit-style applications. The C++ engine ships these; we don't yet.
- Bignum path counts when
i128overflows. - Further perf work. v0.2 hit parity-or-better on every measured workload, with at least one workload running ≥10× faster. The remaining gap on small
from_scratchworkloads (1.5×–1.7× slower than C++ on cold-cache AND-trees of 4–8 variables) is dominated by HashTable churn during structure construction; a flat-array dedup for tiny BConn shapes would close this.
If any of these are interesting to you, see CONTRIBUTING.md.
MIT. See LICENSE.
- Sistla, M., Chaudhuri, S., and Reps, T. CFLOBDDs: Context-Free-Language Ordered Binary Decision Diagrams. ACM TOPLAS 46(2), 2024. arXiv:2211.06818.
- The C++ reference engine: https://github.com/trishullab/cflobdd.