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
    • Bounded and Unbounded Results
      • BMC Unrolling by Hand
      • Choosing a Depth
      • What BMC Cannot Find
      • A Counterexample Always Wins
    • 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

←Bounded and Unbounded Results
  1. Topics
  2. Formal Verification
  3. Proof Engines and Convergence
  4. Bounded and Unbounded Results
  5. A Counterexample Always Wins
On this page

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

    A Counterexample Always Wins

    One FIFO bug under two assumption sets: a counterexample without the assumption and a proof with it, the table of what differed, and the checklist that settles disagreeing runs.

    Why it matters

    The hardest review argument is not about depth but about assumptions: a bounded run fails while an unbounded proof passes, and each side trusts its own run. The resolution is the rule from the first topic. A counterexample on a model always beats a pass on a different model, and an extra assumption makes it a different model. This lesson shows the pair in runnable form, then the table that makes the difference visible.

    The core idea

    On one model, with one set of assumptions and one reset, the bounded engine and the unbounded engine cannot disagree: if the bounded base case finds a trace, the unbounded run reports that trace too, and nothing proves the property. Assume away the failing input and the verdict changes, because the model changed: forbid a push while the FIFO is full and the overflow bug can no longer be triggered, so the same design with the same bug proves. That proof is conditional on the assumption, and it is sound only if something outside the block guarantees the assumed rule. The third run states the same sentence as an assertion on the free inputs and shows that nothing guarantees it.

    Worked example: set A, set B and the assumption as an assertion

    Set A is the FIFO with the push-when-full bug and no assumption. The bounded run finds the counterexample at step 6, and asking the unbounded engine the same question returns the same counterexample, because its base case is the same bounded search. Set B adds one assumption, no push while full, and the same design proves. The last block takes that assumption and asserts it instead: with the inputs free, the assertion fails, so the environment of this run does not provide what set B relied on.

    sync_fifo.sv
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    // formal: bmc 7 expect fail slang top sync_fifo define BUG=1
    `define FORMAL
    // sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
    // Synchronous active-high reset. count is an output so properties can see it.
    // BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
    // empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
    // data bit 7 stuck at 0.
    // This copy keeps the control properties only: no data-integrity harness.
    module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire       push,
      input  wire [7:0] wdata,
      input  wire       pop,
      output wire [7:0] rdata,
      output wire       full,
      output wire       empty,
      output reg  [2:0] count
    );
      reg [7:0] mem [0:3];
      reg [1:0] wptr, rptr;
    
      assign full  = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
      assign empty = (count == 3'd0);
      assign rdata = mem[rptr];
    
      wire do_push = push && (!full  || BUG == 1);
      wire do_pop  = pop  && (!empty || BUG == 2);
    
      always @(posedge clk) begin
        if (rst) begin
          wptr  <= 2'd0;
          rptr  <= 2'd0;
          count <= 3'd0;
        end else begin
          if (do_push) begin
            mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
            wptr      <= wptr + 2'd1;
          end
          if (do_pop) rptr <= rptr + 2'd1;
          if (BUG == 4 && do_push && do_pop) count <= count + 3'd1;   // forgets the pop
          else                               count <= count + {2'b0, do_push} - {2'b0, do_pop};
        end
      end
    
    `ifdef FORMAL
      // ---- harness: first cycle is reset, state before it is arbitrary ----
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // ---- safety properties (after the reset cycle) ----
      always @(*) if (!init) begin
        a_range:   assert (count <= 3'd4);                       // no overflow, no underflow wrap
        a_flags:   assert (full == (count == 3'd4) && empty == (count == 3'd0));
        a_not_both: assert (!(full && empty));
        // occupancy equals pointer distance (mod 4)
        a_ptr:     assert (count[1:0] == (wptr - rptr));
      end
    
      // ---- transition properties (use history: skip the cycle after reset) ----
      always @(posedge clk) if (!init && !$past(rst)) begin
        // push alone when not full adds one; pop alone when not empty removes one
        a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
        a_pop_dec:  assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
        // push into a full FIFO (and no pop) is ignored: occupancy unchanged
        a_ovf_ign:  assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
        // simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
        a_both:     assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
      end
    
    `endif
    endmodule
    sync_fifo.sv
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    // formal: prove 12 expect fail slang top sync_fifo define BUG=1
    `define FORMAL
    // sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
    // Synchronous active-high reset. count is an output so properties can see it.
    // BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
    // empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
    // data bit 7 stuck at 0.
    // This copy keeps the control properties only: no data-integrity harness.
    module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire       push,
      input  wire [7:0] wdata,
      input  wire       pop,
      output wire [7:0] rdata,
      output wire       full,
      output wire       empty,
      output reg  [2:0] count
    );
      reg [7:0] mem [0:3];
      reg [1:0] wptr, rptr;
    
      assign full  = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
      assign empty = (count == 3'd0);
      assign rdata = mem[rptr];
    
      wire do_push = push && (!full  || BUG == 1);
      wire do_pop  = pop  && (!empty || BUG == 2);
    
      always @(posedge clk) begin
        if (rst) begin
          wptr  <= 2'd0;
          rptr  <= 2'd0;
          count <= 3'd0;
        end else begin
          if (do_push) begin
            mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
            wptr      <= wptr + 2'd1;
          end
          if (do_pop) rptr <= rptr + 2'd1;
          if (BUG == 4 && do_push && do_pop) count <= count + 3'd1;   // forgets the pop
          else                               count <= count + {2'b0, do_push} - {2'b0, do_pop};
        end
      end
    
    `ifdef FORMAL
      // ---- harness: first cycle is reset, state before it is arbitrary ----
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // ---- safety properties (after the reset cycle) ----
      always @(*) if (!init) begin
        a_range:   assert (count <= 3'd4);                       // no overflow, no underflow wrap
        a_flags:   assert (full == (count == 3'd4) && empty == (count == 3'd0));
        a_not_both: assert (!(full && empty));
        // occupancy equals pointer distance (mod 4)
        a_ptr:     assert (count[1:0] == (wptr - rptr));
      end
    
      // ---- transition properties (use history: skip the cycle after reset) ----
      always @(posedge clk) if (!init && !$past(rst)) begin
        // push alone when not full adds one; pop alone when not empty removes one
        a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
        a_pop_dec:  assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
        // push into a full FIFO (and no pop) is ignored: occupancy unchanged
        a_ovf_ign:  assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
        // simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
        a_both:     assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
      end
    
    `endif
    endmodule
    sync_fifo.sv
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    // formal: prove 6 expect pass slang top sync_fifo define BUG=1
    `define FORMAL
    // sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
    // Synchronous active-high reset. count is an output so properties can see it.
    // BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
    // empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
    // data bit 7 stuck at 0.
    // This copy keeps the control properties only: no data-integrity harness.
    module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire       push,
      input  wire [7:0] wdata,
      input  wire       pop,
      output wire [7:0] rdata,
      output wire       full,
      output wire       empty,
      output reg  [2:0] count
    );
      reg [7:0] mem [0:3];
      reg [1:0] wptr, rptr;
    
      assign full  = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
      assign empty = (count == 3'd0);
      assign rdata = mem[rptr];
    
      wire do_push = push && (!full  || BUG == 1);
      wire do_pop  = pop  && (!empty || BUG == 2);
    
      always @(posedge clk) begin
        if (rst) begin
          wptr  <= 2'd0;
          rptr  <= 2'd0;
          count <= 3'd0;
        end else begin
          if (do_push) begin
            mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
            wptr      <= wptr + 2'd1;
          end
          if (do_pop) rptr <= rptr + 2'd1;
          if (BUG == 4 && do_push && do_pop) count <= count + 3'd1;   // forgets the pop
          else                               count <= count + {2'b0, do_push} - {2'b0, do_pop};
        end
      end
    
    `ifdef FORMAL
      // ---- harness: first cycle is reset, state before it is arbitrary ----
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // ---- safety properties (after the reset cycle) ----
      always @(*) if (!init) begin
        a_range:   assert (count <= 3'd4);                       // no overflow, no underflow wrap
        a_flags:   assert (full == (count == 3'd4) && empty == (count == 3'd0));
        a_not_both: assert (!(full && empty));
        // occupancy equals pointer distance (mod 4)
        a_ptr:     assert (count[1:0] == (wptr - rptr));
      end
    
      // ---- transition properties (use history: skip the cycle after reset) ----
      always @(posedge clk) if (!init && !$past(rst)) begin
        // push alone when not full adds one; pop alone when not empty removes one
        a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
        a_pop_dec:  assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
        // push into a full FIFO (and no pop) is ignored: occupancy unchanged
        a_ovf_ign:  assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
        // simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
        a_both:     assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
      end
    
      always @(*) if (!init) u_env: assume (!(push && full));   // set B: the environment never pushes into a full FIFO
    `endif
    endmodule
    sync_fifo.sv
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    // formal: bmc 6 expect fail slang top sync_fifo
    `define FORMAL
    // sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
    // Synchronous active-high reset. count is an output so properties can see it.
    // BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
    // empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
    // data bit 7 stuck at 0.
    // This copy keeps the control properties only: no data-integrity harness.
    module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire       push,
      input  wire [7:0] wdata,
      input  wire       pop,
      output wire [7:0] rdata,
      output wire       full,
      output wire       empty,
      output reg  [2:0] count
    );
      reg [7:0] mem [0:3];
      reg [1:0] wptr, rptr;
    
      assign full  = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
      assign empty = (count == 3'd0);
      assign rdata = mem[rptr];
    
      wire do_push = push && (!full  || BUG == 1);
      wire do_pop  = pop  && (!empty || BUG == 2);
    
      always @(posedge clk) begin
        if (rst) begin
          wptr  <= 2'd0;
          rptr  <= 2'd0;
          count <= 3'd0;
        end else begin
          if (do_push) begin
            mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
            wptr      <= wptr + 2'd1;
          end
          if (do_pop) rptr <= rptr + 2'd1;
          if (BUG == 4 && do_push && do_pop) count <= count + 3'd1;   // forgets the pop
          else                               count <= count + {2'b0, do_push} - {2'b0, do_pop};
        end
      end
    
    `ifdef FORMAL
      // ---- harness: first cycle is reset, state before it is arbitrary ----
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // ---- safety properties (after the reset cycle) ----
      always @(*) if (!init) begin
        a_range:   assert (count <= 3'd4);                       // no overflow, no underflow wrap
        a_flags:   assert (full == (count == 3'd4) && empty == (count == 3'd0));
        a_not_both: assert (!(full && empty));
        // occupancy equals pointer distance (mod 4)
        a_ptr:     assert (count[1:0] == (wptr - rptr));
      end
    
      // ---- transition properties (use history: skip the cycle after reset) ----
      always @(posedge clk) if (!init && !$past(rst)) begin
        // push alone when not full adds one; pop alone when not empty removes one
        a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
        a_pop_dec:  assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
        // push into a full FIFO (and no pop) is ignored: occupancy unchanged
        a_ovf_ign:  assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
        // simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
        a_both:     assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
      end
    
      always @(*) if (!init) a_env: assert (!(push && full));   // the same sentence as a property of the free inputs
    `endif
    endmodule

    Run

    Assumptions

    Reset

    Bound

    Verdict

    A, bounded

    None

    One reset step

    Depth 7

    Counterexample at step 6

    A, unbounded attempt

    None

    One reset step

    Depth 12

    Counterexample: the base case finds the same trace

    B

    No push while full

    One reset step

    Depth 6

    Proven under the assumption

    Assumption as assertion

    None; free inputs

    One reset step

    Depth 6

    Counterexample: nothing guarantees the assumption

    Read down the Assumptions column: it is the only column that differs between the failing run and the passing one. The bank scenario follows. A bounded check fails on a 16-entry FIFO at the depth of its first overflow (19 by the rule in the depth lesson) while an unbounded proof passes. Both can be right about different models; the counterexample is right about the model without the assumption, and the proof is a statement about the model with it. Find the assumption the failing trace violates, then ask which neighbour guarantees it.

    What the review looks for

    • Every assumption is listed next to the verdict it conditions, with the block that guarantees it.

    • An assumption on anything but a primary input is challenged: assumptions constrain the environment, never the design.

    • The bank scenario answer is on record: a failure at the first overflow depth against a passing unbounded proof means the proof assumed the failing input away, so the assumption, not the solver, is on trial.

    Verify and debug

    Settle any disagreement with the checklist in order: same design revision and variant, same assumption set, same reset and initial state, same bound, same property text. Then convert every assumption into its discharging assertion on the neighbour and rerun, as the last block does: the assumption that no neighbour proves is the one hiding the bug. A proof that survives a design mutation is the only proof trusted further.

    An assumption that hides a real bug: the block assumes the failing input away instead of asserting that the design tolerates it, and silicon fails where the proof passed. The symptom is a clean block with an escape the neighbour never promised to prevent. The fix is asserting at the block boundary what the neighbour owes, keeping documented interface rules as the only assumptions, found by reviewing every assumption against the neighbour's proven assertions.

    If asked how you decide between assume and assert, answer by side: assume the rules the environment owes on inputs, assert the guarantees the block owes on outputs, and discharge every assumption as an assertion on the neighbour. Over-constraint risks vacuity, under-constraint risks false counterexamples, and the cover list tells which you have.

    Check yourself Your bounded check fails at the first overflow depth of a 16-deep FIFO while the unbounded proof passes. Who is right?

    The counterexample, about its own model. The proof answers a different question: the same design under an extra assumption. Find the assumption the failing trace violates, check whether any neighbour guarantees it, and treat an unguaranteed assumption as the bug until proven otherwise.

    Related topics

    • Previous: What BMC Cannot Find

    • Next: Base Case and Step

    • Formal: Assumptions and Constraints

    • Hardware Debug: Proving the Fix

    Previous

    67 / 183

    Next: K-Induction in Depth

    Up next

    Up nextK-Induction in Depth

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • BMC Unrolling by Hand
    • Choosing a Depth