When a formal check fails, the tool gives you a counterexample: a short waveform that starts at reset and ends with the broken rule. Reading it well is the most important practical skill in formal verification. This article takes a real counterexample from the lab engine, for a FIFO occupancy counter with a bug, and reads it step by step, from the failure back to the cause.
9 min read | Beginner to Intermediate
What You'll Learn
- Find the failing step and the failing assertion in the formal log
- Read a counterexample waveform backwards from the failure to the cause
- Tell the inputs that cause a failure from the inputs that are only there by chance
- Explain why a counterexample is as short as possible
The Design: A FIFO Occupancy Counter
A FIFO (first in, first out buffer) keeps track of how many entries it holds. This is the counter part of a FIFO with 4 entries. A push adds an entry, a pop removes one, full blocks pushes when there is no room, and empty blocks pops when there is nothing to read:
module fifo_count (input clk, input rst, input push, input pop,
output reg [2:0] count, output full, output empty);
assign full = (count == 3'd5); // bug: a 4-entry FIFO is full at 4
assign empty = (count == 3'd0);
always @(posedge clk)
if (rst) count <= 3'd0;
else if (push && !full) count <= count + 3'd1;
else if (pop && !empty) count <= count - 3'd1;
endmodule
If a push and a pop arrive in the same cycle, the push wins. That keeps the example short. A real FIFO would let both happen and keep the count unchanged.
The designer typed 5 instead of 4 in the full condition. The rule that catches it is simple: a FIFO with 4 entries never holds more than 4.
a_count: assert property (@(posedge clk) disable iff (rst) count <= 3'd4);
In the form the lab reads:
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
always @(posedge clk) if (!init) assert (count <= 3'd4);
`endif
Step 1: Read the Log
Run the check at depth 10. The log ends like this (shortened):
SBY [run] engine_0: ## 0:00:00 Checking assertions in step 7.. SBY [run] engine_0: ## 0:00:00 BMC failed! SBY [run] engine_0: ## 0:00:00 Assert failed in fifo_count: design.sv:14.36-14.58 SBY [run] engine_0: ## 0:00:00 Writing trace to VCD file: engine_0/trace.vcd SBY [run] summary: engine_0 (smtbmc bitwuzla) returned FAIL SBY [run] summary: counterexample trace: run/engine_0/trace.vcd SBY [run] summary: failed assertion fifo_count._witness_.check_assert_design_sv_14_15 at design.sv:14.36-14.58 step 7 SBY [run] DONE (FAIL, rc=2)
Three facts come from the log before you open any waveform:
- Which property failed: the one at
design.sv, line 14, columns 36 to 58. That is theassert (count <= 3'd4). When a design has several assertions, the line number tells you which one. - When it failed: step 7. Steps start at 0, so the failure happens in the eighth clock cycle.
- Where the trace is:
trace.vcd, a standard waveform file. The lab opens it for you in the waveform viewer.
The tool stopped at step 7 although the depth was 10. A bounded check looks at step 0, then step 1, and so on, and stops at the first step in which an assertion can fail. So the counterexample is always as short as possible: there is no way to break this rule in fewer than 8 cycles.
Quick Check
Q: A design has five assertions and the check fails. How do you know which assertion failed?
Show Answer
From the log. The "Assert failed" and "failed assertion" lines give the file, line and column of the assertion that failed, and the step in which it failed.
Step 2: Read the Waveform Backwards
A counterexample is read from the end. Start at the failure and ask "why" until you reach a line of RTL. Here is the trace from the lab engine:
Working backwards:
- Step 7: the assertion fails. The value it checked is the one from step 6, where
countis 5. Anassertinside a clocked block samples its signals at the clock edge, so the tool reports the failure one step after the bad value appears. - Why is count 5 in step 6? In step 5,
countwas 4 andpushwas 1. The counter only adds 1 whenfullis 0, andfullwas 0. - Why was full 0 with 4 entries? Because
fullis defined ascount == 3'd5. With 4 entries the FIFO is already full, but the design does not say so. That is the bug.
Three questions led from the symptom to the line of RTL. This is the normal way to debug a counterexample, and with practice it often takes less than a minute.
Step 3: Ignore What Does Not Matter
The trace also shows pop going high in steps 3 and 5. A beginner might think the pops are part of the bug. They are not. The solver picks a value for every input in every step, and any value that does not stop the failure is allowed. In steps 3 and 5 push is also high, and push has priority, so the pops have no effect at all.
To tell the important inputs from the accidental ones, ask: "would the failure still happen if this input had the other value?" For the pops the answer is yes, so they are noise. For the push in step 5 the answer is no: without it, count stays at 4 and the rule holds.
Quick Check
Q: In the counterexample, pop is 1 in step 3 but count still goes up. Is that a second bug?
Show Answer
No. push is also 1 in step 3, and the design gives a push priority over a pop. The pop is a value the solver chose freely; it does not influence the failure.
Step 4: Fix and Rerun
Change the full condition to count == 3'd4 and run again. The run passes: within 10 cycles no input sequence can push the count above 4. Always rerun after a fix. A fix can be incomplete, or it can break a different rule, and the next counterexample shows that at once.
A good habit is to add a second assertion that states the meaning of full directly, so that a similar mistake is caught by a clearer property next time:
a_nofull: assert property (@(posedge clk) disable iff (rst) full |-> count == 3'd4);
The operator |-> means "if the left side is true, the right side must be true in the same cycle". Chapter 3 covers it. In the lab form it is if (!init && full) assert (count == 3'd4);.
Running It in ChipVerify Lab
The example below is the buggy FIFO counter with the assertion. When the check fails, the lab loads the counterexample into the waveform viewer, where you can add count, push, pop and full and step through the cycles yourself.
Common Mistakes
Mistake #1: Reading the counterexample from the start
The early cycles of a trace are usually reset and setup. Start at the failing step and work backwards, asking why each value is what it is. That leads to the cause much faster.
Mistake #2: Treating every input change as part of the bug
The solver chooses all inputs, including ones that play no role. Check each input change by asking whether the failure still happens without it, and ignore the ones that do not matter.
Mistake #3: Fixing the assertion instead of the design
If the rule is right, the counterexample points to a design bug. Relaxing the assertion to count <= 3'd5 makes the check pass and hides the bug. Change a property only when the counterexample shows that the property itself is wrong.
Practice Exercise
Challenge: Debug and Harden the FIFO Counter
Open the example and run it. Find the bug from the counterexample, fix it, and rerun. Then add the assertion "when empty is high, count is 0" and an assertion that full and empty are never high together. Run again.
Hint
In the lab form, a "when A, then B" rule in the same cycle is written as if (!init && A) assert (B);. "Never both" is assert (!(full && empty));.
Solution
The fix is assign full = (count == 3'd4);. The two new assertions are:
always @(posedge clk) if (!init && empty) assert (count == 3'd0);
always @(posedge clk) if (!init) assert (!(full && empty));
All three assertions pass at depth 10.
Open the ready-made example in the lab and run the formal check from the formal panel. The free tier allows a depth of up to 10 cycles, and PRO allows up to 100.
Summary
- The log tells you which assertion failed, by file and line, and in which step.
- A bounded check stops at the first failing step, so a counterexample is the shortest possible way to break the rule.
- A clocked assertion reports a failure one step after the bad value appears, because it samples at the clock edge.
- Read the waveform backwards: from the failure, ask why each value is what it is until you reach a line of RTL.
- Inputs that do not change the outcome are chosen freely by the solver. Ignore them.
- After a fix, always rerun, and fix the design, not the assertion, unless the assertion is wrong.