Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Glossary
  • Metrics Reference
  • Formal Cheat Sheet
  • Formal Foundations19
  • Properties and SVA16
  • Property Development and Constraints19
  • Proof Engines and Convergence23
    • Model Checking in Plain Terms
      • Transition Systems and Reachability
      • BDDs and SAT in Two Pages
      • Safety as Reachability and the Engine Menu
      • Why Results Differ Between Runs
    • Bounded and Unbounded Results
    • K-Induction in Depth
    • IC3, PDR and Interpolation, Concept Level
    • Fairness, Lassos and Liveness-to-Safety
    • Proof Convergence Tactics
  • Formal Applications20
  • Equivalence Checking14
  • Formal Coverage and Signoff15
  • Methodology and Debug13
  • Interview Prep15
  • Capstone: A Chip-Block Verification Lab24
Loading lesson

←Model Checking in Plain Terms
  1. Topics
  2. Formal Verification
  3. Proof Engines and Convergence
  4. Model Checking in Plain Terms
  5. BDDs and SAT in Two Pages
On this page

    Part 4 of 10, lesson 2 of 23Part 4 of 10/Proof Engines and Convergence/Lesson 2 of 23All levels

    BDDs and SAT in Two Pages

    How engines store sets of states compactly and how one design step becomes solver clauses, with a variable-order experiment and a clause-count check.

    Why it matters

    Two runs on the same design can differ in cost by orders of magnitude while proving the same thing, and the reason is representation, not logic. One engine stores state sets as decision diagrams whose size depends on variable order; another unrolls steps into clauses whose count grows with depth. Knowing both explains why a proof stalls and which reduction to reach for.

    The core idea

    A decision diagram (a BDD) stores a Boolean function as a graph of if-then-else nodes, one level per variable, with identical subgraphs shared and decisions that do not matter skipped. Its size depends on the variable order. For (x1 and x2) or (x3 and x4) or (x5 and x6), keeping each pair adjacent gives 6 nodes, while interleaving the pairs (x1, x3, x5, then x2, x4, x6) gives 14: the diagram must remember which pairs have half their inputs set. With four pairs the counts are 8 grouped and 30 interleaved, and the gap keeps widening. Same function, same engine, several times the memory: order is a first-class cost driver, and reordering heuristics exist precisely because of it. Not every function is sensitive: a six-input parity needs 11 nodes under any order.

    A solver-based engine takes a different route. It copies the combinational logic once per cycle, ties the copies together through the registers, and asks for an assignment that breaks a property. A SAT solver works on clauses over single bits; an SMT solver keeps the same unrolling but states constraints on whole vectors (adders, comparators, multiplexers), so there are fewer and wider constraints and the same one-copy-per-cycle structure. As SAT arithmetic, each two-input gate costs about 3 clauses, so 50,000 gates and 2,000 state bits give 3 x 50,000 + 2,000 = 152,000 clauses per step, 4.56 million at depth 30 and 45.6 million at depth 300. This is illustrative arithmetic about the method, not a measurement of any product, and where memory ends a real run depends on the tool and the machine.

    Worked example: order sensitivity and clause arithmetic

    The first script counts the nodes of the reduced diagram exactly: for each variable level it collects the distinct sub-functions that really depend on that variable. It asserts 6 grouped and 14 interleaved for the pair function, 8 and 30 for four pairs, and, as a failing partner, that parity is not order-sensitive: asserting that it is gets caught. The second script wraps the clause arithmetic in a checker; a model that forgets the register-link equalities fails the same checker.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    from itertools import product
    
    def bdd_nodes(f, n, order):
        """Internal nodes of the reduced ordered diagram of f under a variable order."""
        nodes = set()
        for k in range(n):
            for prefix in product((0, 1), repeat=k):
                fixed = dict(zip(order[:k], prefix))
                rest = order[k:]
                table = []
                for vals in product((0, 1), repeat=len(rest)):
                    a = dict(fixed)
                    a.update(zip(rest, vals))
                    table.append(f(a))
                half = len(table) // 2
                lo, hi = tuple(table[:half]), tuple(table[half:])
                if lo != hi:  # this sub-function really depends on order[k]
                    nodes.add((k, tuple(table)))
        return len(nodes)
    
    def pair_or(npairs):
        return lambda a: int(any(a[2 * i] and a[2 * i + 1] for i in range(npairs)))
    
    def parity(a):
        return sum(a.values()) % 2
    
    def order_sensitive(f, n, o1, o2):
        return bdd_nodes(f, n, o1) != bdd_nodes(f, n, o2)
    
    grouped = [0, 1, 2, 3, 4, 5]
    interleaved = [0, 2, 4, 1, 3, 5]
    assert bdd_nodes(pair_or(3), 6, grouped) == 6
    assert bdd_nodes(pair_or(3), 6, interleaved) == 14
    g4 = list(range(8))
    i4 = [0, 2, 4, 6, 1, 3, 5, 7]
    assert bdd_nodes(pair_or(4), 8, g4) == 8
    assert bdd_nodes(pair_or(4), 8, i4) == 30
    assert order_sensitive(pair_or(3), 6, grouped, interleaved)
    
    # injected data: parity is order-insensitive, so claiming otherwise must be caught
    assert bdd_nodes(parity, 6, grouped) == 11
    caught = False
    try:
        assert order_sensitive(parity, 6, grouped, interleaved)
    except AssertionError:
        caught = True
    assert caught, "sensitivity check accepted an order-insensitive function"
    print("PASS: pair function 6 nodes grouped, 14 interleaved; parity 11 in any order")
    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def clauses(gates, flops, depth, link_flops=True):
        per_step = 3 * gates + (flops if link_flops else 0)
        return per_step * depth
    
    def check_projection(model):
        assert model(50000, 2000, 1) == 152000
        assert model(50000, 2000, 30) == 4560000
        assert model(50000, 2000, 300) == 45600000
    
    check_projection(clauses)
    
    # injected model: forgets the register-link equalities
    def forgetful(gates, flops, depth):
        return clauses(gates, flops, depth, link_flops=False)
    
    caught = False
    try:
        check_projection(forgetful)
    except AssertionError:
        caught = True
    assert caught, "projection check accepted a model without register links"
    print("PASS: 152000 clauses per step, 4.56 million at depth 30, 45.6 million at depth 300; miscount caught")

    What the review looks for

    • Cost is blamed on representation or search, never on the property being true.

    • Any claim that one order or encoding is better comes with two counted numbers, not adjectives.

    • Clause projections are labelled illustrative unless they were read from a run.

    Verify and debug

    Recheck a stalled symbolic run by asking for the state-bit count in the cone of the property first: a jump from hundreds to thousands of bits points at the cone, not the engine. For solver-based runs compare the depth against the clause projection above; if the run is far slower than the projection suggests, the difficulty is search order inside the solver, and splitting or strengthening helps more than waiting.

    A run that never completes after a datapath register was widened from 8 to 32 bits: the cone of an untouched control property now holds 24 more state bits, so 2^24 times as many states. The symptom is a proof that passed in minutes and now exhausts memory. The fix is abstraction or parameter reduction on the datapath, covered in the convergence topic, found by comparing the cone bit counts before and after the change.

    If asked why formal tools scale badly, do not say exponential and stop. Say what grows: diagram nodes with variable order, clauses with gates times depth, and that both are fought by shrinking the cone, abstracting data, and strengthening induction, each with a number attached.

    Check yourself The same function needs 6 nodes under one order and 14 under another. What does that tell you about a slow symbolic run?

    That representation dominates: try reordering or restructuring before concluding the property is hard. The logic is identical; only the storage changed.

    Related topics

    • Previous: Transition Systems and Reachability

    • Next: Safety as Reachability and the Engine Menu

    • Formal: Abstraction Techniques

    Previous

    61 / 183

    Next: Safety as Reachability and the Engine Menu

    Up next

    Up nextSafety as Reachability and the Engine Menu

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Why Results Differ Between Runs