Over-Constraint and Vacuity
Hide a FIFO bug behind one assumption, find it again with the honest one, and let a cover tell the two apart.
- Lesson
- F3.2
- Time
- 15 min
- Runs on
- SymbiYosys
- Check
- 4 questions
A FIFO bench carried assume(!(push && count == 16)) from its first week, added to silence a failure until the producer model was ready. The failure was the bug: the exported full lagged the count by one cycle, so an obedient producer could push a seventeenth entry. Every property passed for four months, and one cover on "push while the count is sixteen" would have come back unreachable on day one.
Measured with smtbmc yices at depth 20. Steps 1 to 16 are one push each from reset. The sampled violation is step 18, reported at 19: the last sample a depth-20 run checks.
Too Many Assumptions
Read the diagram as text
- No assumption: pushes into a full FIFO allowed
- assume(!(push && full)): reads the port the producer can see
- assume(!(push && count == 16)): reads a register the producer cannot see
- False failures: illegal producers (F3.3)
- Every legal producer: including the one that pushes in the lag cycle
- Every legal producer but one: the push that breaks the FIFO is gone
- No assumption → False failures
- assume(!(push && full)) → Every legal producer
- assume(!(push && count == 16)) → Every legal producer but one
The right-hand zone is over-constraint: the proof is true, of a world the hardware never lives in. A pass looks the same in both right-hand columns. A cover does not.
An assumption removes input sequences. Remove the right ones and the tool checks the legal environment. Remove one too many and it checks a world the chip never sees: over-constraint. Nothing in a PASS says which zone you are in (F1.3).
The internal assumption also breaks the master rule of F3.1: it constrains the design through its own register. The FIFO bug lives exactly in the gap between full and count == 16.
| Run | Measured result | Time |
|---|---|---|
A: assume(!(push && count == 16)), assert count <= 16 | BMC PASS to depth 20 | 535 ms |
B: assume(!(push && full)), assert count <= 16 | FAIL at reported step 19 | 577 ms |
| B in concurrent SVA (props.sv) | FAIL at reported step 19 | 686 ms |
restrict(count < 16) instead of an assumption | BMC PASS to depth 20 | 533 ms |
Fixed RTL (full from the count), honest assumption | BMC PASS to depth 20 | 535 ms |
fifo16 with the lagging full; mode bmc, depth 20, smtbmc yices, reset assumed in step 0.
restrict is an assumption that only formal honours, and it hides the bug just as well. It exists for case splits (F4.3), never for silencing a failure.
The Cover Is the Alarm
The honest run, steps 16 to 18. The internal assumption forbids the push at step 17; the honest one allows it, because full is still 0.
push=1, pop=0
count=15, full=0count <= 16 holds
✓ Holdspush=1, pop=0
count=16, full=0internal assumption: no push here
Assumed for inductionpush=0, pop=0
count=17, full=1count <= 16 fails
× Fails
Select a frame to inspect its inputs and assertion.
Measured values from data/F3-2.json. SBY reports the step-18 failure at step 19.
| Cover | Under the internal assumption | Under the honest assumption |
|---|---|---|
full | reached, reported step 19 | reached, reported step 19 |
count == 16 && push | Unreached cover statement at top: c_push_at16 | reached, reported step 18 |
pop && empty | reached, reported step 2 | reached, reported step 2 |
Mode cover, depth 20, the same buggy RTL. Measured: 602 ms and 641 ms.
For every behaviour an assumption touches, cover the case you still expect to happen. A push at sixteen is legal for this producer, and the internal assumption made it unreachable. SBY names the unreached cover in its log: that line is the alarm.
Vacuity in Formal
Vacuous in simulation
push && count == 16 |-> ...
// this test never filled the FIFO
One test's stimulus never made the antecedent true.
Vacuous in formal
assume(!(push && count == 5'd16));
// no legal input sequence fills it and pushes
No input sequence the assumptions allow, within the bound, makes it true.
In simulation (A1.3 Implication and vacuous success) vacuity is a weak test. In formal it is a statement about the whole environment, caused by the assumptions or by dead logic (F3.5). Commercial tools flag some of it automatically; in SBY you write the cover yourself, so make it a habit.
The habit is cheap: one cover per assumption, naming the legal behaviour closest to what the assumption forbids. For !(push && full) that is a push at sixteen, which an honest producer may still make in the lag cycle.
Asked in Interviews
| Question | Answer |
|---|---|
| What is over-constraining and how do you detect it? | Assumptions that remove legal behaviour. A cover on behaviour you expect comes back unreachable. |
| What is vacuity in formal? | A pass whose antecedent no legal input sequence can reach under the assumptions. |
Can restrict hide a bug? | Yes. To the prover it is an assumption. |
Try It
Concurrent SVA
assert property (@(posedge clk) disable iff (rst) count <= 5'd16);
assume property (@(posedge clk) disable iff (rst) !(push && full));
// the dishonest variant:
assume property (@(posedge clk) disable iff (rst) !(push && count == 5'd16));
One claim, and the producer promise written two ways.
The clocked immediate form the editor runs
always @(posedge clk) if (!rst) begin
assume(!(push && count == 5'd16));
assert(count <= 5'd16);
end
No delays, so each line maps one to one.
mode cover; the depth stays at the default, 20.[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top fifo16
[files]
design.svRun the internal assumption
Run as it is: BMC PASS to depth 20. Fixed?
Cover under it
Swap the assert for the three covers and set mode cover. Read which cover the log names as unreached.
Honest assumption
Back in mode bmc, assume
!(push && full)instead: FAIL at reported step 19. Find the lag cycle in the trace.Cover under the honest one
Run the three covers again: all reached.
Fix the RTL
Drive
fullfromcount == 16combinationally and rerun the honest bench: BMC PASS to depth 20.
design.sv
props.sv
Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.
Inputs are free unless constrained. Run to find a counterexample or check a proof.
Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.
SBY summary and log
Check Yourself
Remember
- Over-constraint: an assumption removes legal behaviour, and the proof becomes true of a world the chip never sees.
- Write assumptions on ports the neighbour can see, never on the design's internal state.
- For every assumption, cover the behaviour you still expect; an unreached cover is the alarm.
restrictis an assumption to the prover and can hide a bug just as well.