Formal with Simulation
Turn a block assumption into a system assertion with one line and a bind, measure what is lost without it, replay a formal cover trace as a directed test, and decide what stays in simulation.
- Lesson
- F7.4
- Time
- 20 min
- Runs on
- SymbiYosys
- Check
- 5 questions
The FIFO was proven under one assumption: the producer never pushes when full. A DMA engine, tired of waiting, pushed into a full FIFO once. Nothing fired in system simulation, because the assumption lived only in the formal bench.
Three Things Cross Between the Benches
Read the diagram as text
- assume at the block: the producer never pushes when full
- assert in the system: the same line, bound into the FIFO
- cover trace: a reachable state formal found
- directed test: the trace replayed in a simulator
- system failure: a symptom seen in simulation or the lab
- cover question: how could this state be reached? (F5.9)
- one property file: a define picks the verb
- assume at the block → one property file
- one property file → assert in the system
- cover trace → directed test: trace_tb.v
- system failure → cover question
A block proven under an assumption is proven only where the assumption holds. The system bench is where that gets checked.
Formal proves the block; simulation runs the system. An assumption at the block becomes an assertion in the system. A cover trace becomes a directed test. A failure seen in the system becomes a cover for formal to explain.
One Line, Two Verbs
FORMAL, so the line is an assumption there and an assertion everywhere else.module fifo_rules(input clk, rst, push, full);
`ifdef FORMAL
`define PROMISE assume // block level: the producer's promise is an assumption
`else
`define PROMISE assert // system level: the same line, checked
`endif
p_no_push_full: `PROMISE property (@(posedge clk) disable iff (rst) !(push && full));
else $display("PROMISE failed: push while full, cycle %0d", $root.tb.cycles);
endmodule
bind fifo_nf fifo_rules u_rules(.clk(clk), .rst(rst), .push(push), .full(full));| fifo_nf, no full guard | Measured result |
|---|---|
With the promise as an assumption, abc pdr | Prove PASS (abc pdr) |
Without it, bmc depth 20 | FAIL at reported step 7: used reaches 5 |
The FIFO is correct only under the promise. Its proof is a deal: the system must keep its side.
Measured: the FIFO alone, without the promise, bmc depth 20: FAIL at reported step 7. The solver plays the producer and breaks the promise at once.
This FIFO has no full guard, which saves logic on its write path. Its proof holds only while every producer keeps the promise. Bound into the system bench, the same line checks every producer on every cycle.
The Promise, Checked in the System
Read the diagram as text
- cycle 10: push while full: the promise assertion fires here
- used = 5, full drops: the extra write overwrites the oldest entry
- producer keeps pushing: largest used: 7
- cycle 17: sink error: expected 4, read 12
- cycle 10: push while full → used = 5, full drops
- used = 5, full drops → producer keeps pushing
- producer keeps pushing → cycle 17: sink error
Verilator runs of the system bench (40 items, a sink that reads in bursts). The symptom names the FIFO; the cause was the producer.
| System bench on Verilator | Promise assertion | Sink | Largest used |
|---|---|---|---|
| Faulty producer, promise bound | fires at cycle 10 | error at cycle 17 | 7 |
| Correct producer, promise bound | silent; full high for 106 cycles | no error | 4 |
| Faulty producer, no bind | not there | error at cycle 17 | 7 |
Verilator 5.052. The second row is a real pass: the promise was tested on every one of those cycles (A1.3).
With the bind, the failure names the producer at the cycle it broke the promise. Without it, the first sign is 7 cycles later and points at the FIFO, whose proof says it is correct. That gap is the cost of a promise nobody checks.
A Cover Trace Becomes a Test
| Replayed step | push | pop | used | full |
|---|---|---|---|---|
| 1 | 1 | 1 | 0 | 0 |
| 2 | 1 | 0 | 1 | 0 |
| 3 | 1 | 0 | 2 | 0 |
| 4 | 1 | 0 | 3 | 0 |
| 5 | 0 | 1 | 4 | 1 |
| 6 | 0 | 0 | 3 | 0 |
SBY's trace0_tb.v for the cover "full, then a read" (Cover reached, reported step 6), run on Verilator 5.052. It needed one edit: Verilator rejects it with "Duplicate declaration of signal: 'genclock'", so the duplicate reg genclock = 1; line was deleted.
Step 5 matches the formal trace: four entries, full high, a read. Any simulator can now reach the state the solver found, which turns a formal cover into a regression test.
What Stays in Simulation
| Formal, at the block | Simulation, at the system |
|---|---|
| control logic, handshakes, data integrity | the check that every block assumption holds |
| corner cases within the depth budget | long scenarios across many blocks |
| proofs for all time where they close | performance, software bring-up, anything too deep |
The answer for each block is a column in the plan (F7.5).
Coverage combines by direction. A bin that formal proves unreachable is waived from the simulation goal (F5.7); a bin that simulation hits proves nothing beyond that trace. A proven block counts toward sign-off only with its assumptions checked in the system.
Semiformal runs sit between the two: simulate to a deep state, then search formally from there. They find bugs; they prove nothing about states the trace did not reach.
Asked in Interviews
| Question | Answer |
|---|---|
| How does an assumption at one level become an assertion at the next? | The same property text with the verb picked by a define, bound into the system bench. The block proof is valid only where that assertion never fires. |
| What does a formal cover trace give the simulation team? | A directed test that reaches a state formal found. SBY writes it as a Verilog testbench; on Verilator it needed one edit. |
| What is semiformal verification? | Simulation to a deep state, then formal search from it. Deep bug hunting, not a proof. |
Try It
The lab is fifo_nf with the promise in props.sv as an assumption, abc pdr: Prove PASS (abc pdr) in 0.7 s. The simulation twin is the system bench with the faulty producer and the promise bound as an assertion.
assume property (@(posedge clk) disable iff (rst) !(push && full)); // props.sv, formal
assert property (@(posedge clk) disable iff (rst) !(push && full)); // system benchName the assumption
Read props.sv and say, in one sentence, what the FIFO needs from its producer.
Remove it
Clear props.sv, set
mode bmc, depth 20, and run. Readusedat the failing step.Check it in the system
Run the simulation twin. Then delete the
bindline and run again: count the cycles between the two messages.
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
- A block proof holds only where its assumptions hold; bind every block assumption into the system bench as an assertion.
- One property file, a define for the verb: assume in the formal bench, assert in simulation.
- Without the bound assertion the symptom appears later and names the wrong block; with it the failure names the cause at its cycle.
- A formal cover trace replays as a directed test; simulation keeps the assumption checks, long scenarios and performance.