Why Proofs Do Not Converge
Name the three reasons a proof stalls, compute the depth a bounded proof needs, and watch a cut turn a timer that blocks every engine into a half-second answer.
- Lesson
- F4.1
- Time
- 20 min
- Runs on
- SymbiYosys
- Check
- 6 questions
The watchdog passed bmc at depth 40 and was signed off. In silicon, a kick that landed in the very cycle the timer expired still reset the chip. The shortest timeout was 65,536 cycles, so no 40-step run ever reached an expiry, and the report never said so.
The Cone of Influence
Read the diagram as text
- pre, ticks: captured at start, each at least 256
- pre_q × ticks_q: 16 × 16 multiplier
- timer: 32-bit counter, cleared by a kick
- expired: timer == product: the cut point
- kick, start: free inputs
- state: IDLE, RUN, BARK
- assertion: kick in RUN |=> !bark
- pre, ticks → pre_q × ticks_q
- pre_q × ticks_q → expired
- timer → expired
- expired → state: cut here
- kick, start → state
- kick, start → timer: clears
- state → assertion
Every block drawn is in the cone. Yosys keeps 67 flop bits after prep; with expired cut, 3. The depth the timer adds is what no bound reaches.
The cone of influence of an assertion is every signal that can reach it through gates and flops, back to the inputs. Tools delete everything else before an engine starts. What matters is what the cone contains, and how far back in time it reaches.
Three things stall a proof. Sequential depth: a counter puts behaviour thousands of cycles away. Hard logic in the cone: a multiplier, a wide compare. And a property that is not inductive (F2.4, F4.6). This watchdog has the first two.
Depth: The Bound Against the Bug
Read the diagram as text
- step 0: reset: assumed
- step 1: start: pre and ticks captured, each at least 256
- steps 2 to 65,537: RUN: timer counts 0 to 65,535
- step 65,538: kick on expiry: the faulty FSM tests the expiry first
- step 65,539: bark: the violation; reported at step 65,540
- depth 40 ends at sample 38: required depth: 65,541
- step 0: reset → step 1: start
- step 1: start → steps 2 to 65,537
- steps 2 to 65,537 → step 65,538: kick on expiry
- step 65,538: kick on expiry → step 65,539: bark
- depth 40 ends at sample 38 → steps 2 to 65,537: stops inside the wait
Depth 40 covers 0.06 % of the bound the bug needs. The steps on the real timer follow from the measured cut trace plus 65,536 counting cycles.
| Run on the faulty watchdog | Measured result |
|---|---|
bmc, depth 20 | BMC PASS to depth 20, 1.2 s |
bmc, depth 40 | BMC PASS to depth 40, 5.8 s |
bmc, depth 60 | time limit (20 s), no verdict |
cover of state == RUN && expired, depth 40 | cover unreached within depth 40, 5.4 s |
prove, k-induction, depth 20 | UNKNOWN at depth 20, 1.1 s |
prove, abc pdr | time limit (20 s), no verdict |
All smtbmc yices except the last row. The design has a real bug, and not one run says FAIL.
The shortest timeout is 256 × 256 = 65,536 cycles. The cut run below fails at step 4; on the real timer the same failure sits 65,536 steps later, so a bounded proof needs depth 65,541.
This is the proof-depth argument: derive the required bound from the deepest counter and the longest transaction before you trust a bounded PASS, and back it with covers. Here the expiry cover is unreached, so the runs never exercised the logic that matters.
Two Ways a Run Fails to Finish
bmc depth, smtbmc yices | 5 | 10 | 15 | 20 | 80 |
|---|---|---|---|---|---|
| fifo4sb: 4 × 8 FIFO with its scoreboard (F3.4) | BMC PASS to depth 5, 0.5 s | BMC PASS to depth 10, 0.9 s | time limit (20 s), no verdict | time limit (20 s), no verdict | – |
| ctr10: a modulo-10 counter (F1.6) | – | – | – | BMC PASS to depth 20, 0.5 s | BMC PASS to depth 80, 0.6 s |
Same tool, same mode, same depths. The cone decides.
The watchdog fails the first way: the bound is too deep to reach, and every affordable depth says PASS, which means nothing. The spine FIFO fails the second way: a modest depth is too slow to reach, and the run ends with no verdict at all.
Both are called "the proof does not converge", and they have different cures. A deep bound needs abstraction of the deep element (F4.2, F4.5). A slow solver needs an invariant (F4.6) or another engine (F4.7).
The Cone: What Every Step Carries
Design, bmc depth 40 | Measured result |
|---|---|
faulty, limit pre_q * ticks_q | BMC PASS to depth 40, 5.8 s |
faulty, limit {ticks_q, pre_q} | BMC PASS to depth 40, 0.7 s |
fixed, limit pre_q * ticks_q | BMC PASS to depth 40, 0.7 s |
Same timer, same depth. Only the logic in the cone, or the need to reason through it, changes.
Each unrolled step copies the whole cone. To print PASS for the faulty design, the solver must show that timer never equals the product within 40 steps: arithmetic, in every copy. With a concatenated limit that claim is trivial. On the fixed design no failure depends on it, so the solver never asks.
Cut the Timer
Real timer
wire expired = timer == pre_q * ticks_q;
Faulty design: BMC PASS to depth 20, 1.2 s at depth 20.
Cut point
wire expired = $anyseq;
Faulty design: FAIL at step 4, 0.5 s.
Measured counterexample with the timer cut (bmc, depth 20): FAIL at step 4, 0.5 s. expired is now a free input, shaded like the others.
A cut point makes expired a free input: any value in any cycle. Every behaviour of the real timer is still possible, plus more. In the trace the solver raises expired in the first RUN cycle together with a kick, and the FSM barks at step 3.
| After the cut | Measured result |
|---|---|
faulty, timer cut, bmc depth 20 | FAIL at step 4, 0.5 s |
fixed (kick tested first), timer cut, prove depth 2 | Prove PASS, 0.5 s |
fixed, real timer, prove depth 2 | Prove PASS, 0.5 s |
The faulty twin still fails after the cut: the cut hid nothing.
Is the failure real? It needs only a kick in an expiry cycle, which the real timer reaches after 65,536 cycles. So it is a real bug, found in 0.5 s. The last row shows k-induction never needed the timer's value at all. F4.2 shows a cut whose failure is false.
Asked in Interviews
| Question | Answer |
|---|---|
| Why does my proof not converge? | Something in the cone is deep (counters, long transactions), hard (arithmetic, memories), or the property is not inductive. Find which: depth shows in unreached covers, hard logic in solver time, non-induction in an UNKNOWN with a strange step 0. |
| What is a proof-depth argument? | The bound a bounded proof must reach, computed from the deepest counter and the longest transaction. A bounded PASS below it says nothing about the deep behaviour; here the bug needs depth 65,541. |
| What is the cone of influence? | Every signal that can affect the property. Tools prune the rest automatically, so convergence depends on what is inside the cone, not on design size. |
Try It
The lab is the faulty watchdog, bmc at depth 20 with smtbmc yices. Measured: BMC PASS to depth 20, 1.2 s. The rule in concurrent SVA is shown first; the design carries its immediate form.
f_kick plus an immediate assertion.assert property (@(posedge clk) disable iff (rst)
state == RUN && kick |=> !bark);Read the claim
Note what BMC PASS at depth 20 promises, and what the required bound is.
Cut the timer
Replace the right side of
wire expired = ...with$anyseq;and run again. Read the step wherekickandexpiredare both 1.Fix and prove
Swap the two tests in the RUN branch so the kick wins, set
mode proveand depth 2, and run with the cut and without it.
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
- Convergence depends on what is in the cone, not on design size.
- Depth, hard logic and non-inductive properties are the three causes; each has its own symptom.
- Compute the required bound before trusting a bounded PASS, and prove covers that reach it.
- A cut point adds behaviour: it never hides a bug, and its failures must be checked for realism.