An assumption removes input sequences from the search. When it removes only impossible ones, the check becomes accurate. When it removes legal ones too, the check becomes blind: bugs that need those inputs are never found, and every assertion still passes. This is called over-constraining, and it is the most dangerous mistake in formal verification, because it produces a clean result. This article shows an over-constrained check that hides a real bug, and the ways to notice it.
9 min read | Intermediate
What You'll Learn
- Explain how an assumption that is too strong hides bugs
- Find an over-constraint with a cover or a check that is meant to fail
- Recognize contradictory assumptions and how the tool reports them
- Explain why a contradiction in a single state goes unreported
Too Strong Means Blind
Each assumption cuts away part of the input space. The goal is to cut exactly the impossible part. An assumption that cuts more leaves a smaller search, and the tool only checks what is left.
The problem is that the result looks the same either way: PASS. A correct check and an over-constrained check cannot be told apart by their assertion results. You need a separate signal, and that signal is reachability: can the situations the specification allows still happen under the assumptions?
The Design: a Tracker With an Off-by-One
This is the transaction tracker from the input protocol assumptions article, with a 3-bit counter and a bug in the limit check:
module txn3 (input clk, input rst, input go, input resp,
output req, output [2:0] outstanding);
reg [2:0] cnt;
assign outstanding = cnt;
assign req = go && (cnt <= 3'd3); // bug: should be cnt < 3
always @(posedge clk)
if (rst) cnt <= 3'd0;
else if (req && !resp) cnt <= cnt + 3'd1;
else if (!req && resp) cnt <= cnt - 3'd1;
endmodule
With 3 requests in flight, a new go still lets a fourth one out. The rule cnt <= 3 catches it, as long as the tool may raise go while 3 requests are in flight. The specification allows that: the block that drives go does not know the count, and the tracker is supposed to block the request itself.
The Over-Constrained Check
An engineer adds an assumption that seems harmless, "go only when there is room":
m_resp: assume property (@(posedge clk) disable iff (rst) resp |-> cnt != 3'd0);
m_room: assume property (@(posedge clk) disable iff (rst) go |-> cnt < 3'd3); // too strong
a_max: assert property (@(posedge clk) disable iff (rst) cnt <= 3'd3);
The run passes. Of course: m_room forbids exactly the situation in which the bug happens. The assumption is not in the specification. It describes what the engineer expected the neighbor to do, not what the neighbor promises.
Quick Check
Q: With m_room in place, could any bug in the limit check ever be found?
Show Answer
No. The limit check matters only when go arrives while the tracker is full, and m_room removes every such case. The assumption makes the limit logic unobservable.
Noticing It: Reachability
The question to ask is: "can go arrive while 3 requests are in flight?". The specification says yes. In SVA it is a cover:
c_go_full: cover property (@(posedge clk) disable iff (rst) go && cnt == 3'd3);
In cover mode, SymbiYosys reports it unreachable under the assumptions:
SBY [run] engine_0: ## 0:00:00 Checking cover reachability in step 9.. SBY [run] engine_0: ## 0:00:00 Unreached cover statement at txn3: design.sv:23.36-23.61 SBY [run] summary: unreached cover statements: SBY [run] DONE (FAIL, rc=2)
A cover that the specification allows but the check cannot reach is the signature of over-constraining. Remove m_room and the run fails at step 6:
Three requests go out in steps 1 to 3. In step 4, go is high with 3 in flight, the faulty test lets a fourth request out, and the count is 4 in step 5. Change the test to cnt < 3'd3 and the run passes without the extra assumption.
Contradictory Assumptions
The extreme case is a set of assumptions that can never be met together, for example assume (go) and assume (!go) from two different engineers. Then no input sequence is legal, and every assertion holds trivially. SymbiYosys detects this when it happens from the start and stops with an error:
SBY [run] engine_0: ## 0:00:00 Checking assumptions in step 1.. SBY [run] engine_0: ## 0:00:00 Assumptions are unsatisfiable! SBY [run] DONE (ERROR, rc=16)
The lab reports this run as ERROR. A contradiction that only happens in some states is not reported at all. Suppose an assumption can never be met when the count is 2. Every trace that reaches count 2 is then cut off, silently, and the tool never looks beyond it. On the buggy tracker, such an assumption makes the check pass with no warning, because the bug needs the count to pass through 2. Only covers for the states after it reveal the problem.
Quick Check
Q: Why does a contradiction that occurs in one state not produce the "unsatisfiable" error?
Show Answer
Because the assumptions can still be met in other states: every trace that avoids that state is legal. The tool simply never follows a trace into it. Nothing is wrong from the tool's point of view, so it has nothing to report.
Running It in ChipVerify Lab
The lab panel runs assertions, not covers, but you can ask a reachability question with a check that is meant to fail: assert the negation of the situation. If the assertion fails, the situation is reachable, and the counterexample is a trace that reaches it.
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// the memory only responds to a request that is in flight
always @(*) if (!init) assume (!resp || cnt != 3'd0);
// OVER-CONSTRAINED: "go only when there is room"
always @(*) if (!init) assume (!go || cnt < 3'd3);
// never more than 3 in flight
always @(posedge clk) if (!init) assert (cnt <= 3'd3);
// reachability, meant to fail: go while 3 are in flight
always @(posedge clk) if (!init) assert (!(go && cnt == 3'd3));
`endif
With m_room, everything passes, including the check that was meant to fail. That pass is the warning. Remove m_room and the run fails at step 5 on the reachability check, which shows that the situation is now in the search. Comment out the reachability check and the real assertion fails at step 6.
Common Mistakes
Mistake #1: Trusting a clean pass
An over-constrained check passes as cleanly as a correct one. Every formal result needs reachability evidence for the situations the assertions are about.
Mistake #2: Assuming what the neighbor "would sensibly do"
"go only when there is room" sounds sensible, but the specification puts that job inside the tracker. Assume only what the neighbor is required to do.
Mistake #3: Adding assumptions late without re-checking covers
An assumption added to fix one counterexample can silently cut off many other behaviors. Rerun the covers, or the meant-to-fail checks, after every new assumption.
Practice Exercise
Challenge: Find the Assumption That Hides the Bug
Open the example and run it: everything passes, including the reachability check. Decide which assumption is too strong, using the specification: "the memory responds only to requests in flight; go may be high at any time; the tracker must keep at most 3 requests in flight". Remove it, run, fix the bug, and run again.
Hint
Compare each assumption with the specification sentence by sentence. Which one restricts go?
Solution
The assumption !go || cnt < 3'd3 is not in the specification, which says go may be high at any time. Remove it: the reachability check fails at step 5, and with that check commented out, cnt <= 3 fails at step 6. Fix the limit test to assign req = go && (cnt < 3'd3);. Now cnt <= 3 passes, and the reachability check still fails, which is what you want: go while full is a legal situation, and the tracker now handles it.
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
- An assumption that removes legal inputs hides every bug that needs them, and the check still passes.
- Assertion results cannot reveal over-constraining. Reachability can: covers, or in the lab, checks that are meant to fail.
- The off-by-one tracker passed with "go only when there is room" and failed at step 6 without it.
- Assumptions that contradict each other from the start stop the run with "Assumptions are unsatisfiable!".
- A contradiction in one state is silent: it cuts off every trace through that state. Only covers beyond it reveal it.