A formal tool reads the same SVA as a simulator but uses each directive differently. There is no stimulus: the tool considers every input sequence the assumptions allow.
Directive | Formal tool | Simulation |
|---|---|---|
assert property | Prove it holds on every allowed trace, or produce a counterexample | Check it on the traces the tests produce |
assume property | Constraint on inputs; not proved (16.14.2) | Checked and reported like an assert (16.14.2) |
restrict property | Constraint, same as assume | Not checked; has no action block (16.14.4) |
cover property | Find a trace that reaches it, or prove none exists | Count how often it happens |
restrict exists for case splits: prove the ALU with restrict property (@(posedge clk) ctr == '0); for additions in one run and subtraction in another, without simulation flagging tests that use ctr = 1 (the LRM's own example). Verilator 5.048 compiled restrict and ignored it in simulation, as specified.
A safety property fails at a finite time: there is a cycle where you can point and say "here it broke". A liveness property says something good eventually happens; on an infinite trace a violation cannot be pinned to any cycle (16.12.15).
Property | Kind | Can fail in a finite simulation? |
|---|---|---|
| Safety (bounded response) | Yes, four cycles after req |
| Weak sequence: safety with no deadline | Never: an unmatched weak sequence has not failed |
| Liveness (strong) | Only at the end of the run; what a simulator reports for a strong attempt still pending at $finish is tool-dependent |
| Liveness (strong) | Same as s_eventually |
The second row catches people. In an assert or assume, a sequence without strong or weak is treated as weak (16.12.1), so ##[1:$] gives no failure if the grant never comes. In cover, the same sequence is strong.
Formal tools prove liveness directly, usually with fairness assumptions on the environment. Simulation cannot. The practical answer is to write both: a bounded version that simulation and bounded proofs can check, and an s_eventually version for the liveness proof.
// not-run: Verilator 5.048 reports "Unsupported: s_eventually (in property expression)"; Icarus 13 has no concurrent assertions
// Formal harness for the arbiter below.
module rr_arb_formal (input logic clk, rst_n, input logic [3:0] req);
logic [3:0] gnt;
rr_arb dut (.*);
for (genvar i = 0; i < 4; i++) begin : g
// Environment: a request stays up until granted.
m_hold: assume property (@(posedge clk) disable iff (!rst_n) req[i] && !gnt[i] |=> req[i]);
// Liveness: every request is eventually granted.
a_live: assert property (@(posedge clk) disable iff (!rst_n) req[i] |-> s_eventually gnt[i]);
// Safety with a deadline: the stronger, design-specific claim.
a_wait: assert property (@(posedge clk) disable iff (!rst_n) req[i] && !gnt[i] |-> ##[1:3] gnt[i]);
c_wait: cover property (@(posedge clk) disable iff (!rst_n) req[i] && !gnt[i] ##1 !gnt[i] ##1 !gnt[i] ##1 gnt[i]);
end
// Optional case split for one proof run: only requester 0 and 3 active.
r_split: restrict property (@(posedge clk) req[2:1] == 2'b00);
endmoduleSynthesizable aux code only. Formal tools read RTL-like code. No classes, $urandom, # delays, fork, queues or DPI in the logic an assertion depends on.
Assume inputs, assert outputs. An assumption on a DUT output hides bugs. Keep assumptions to the interface the environment drives.
Cover every assumption's purpose. Too-strong assumptions make proofs vacuous. Covers on interesting states (a grant after a wait, a full FIFO) prove the constrained space still contains them.
Bound what you can. Deep $past chains, wide counters and long ranges grow the state space. A bounded response window is often easier to prove than unbounded liveness, and it is what the spec usually means.
Track one symbolic transaction. Instead of a table for every ID, track one ID chosen by the tool and held constant; a proof for an arbitrary ID covers them all.
Assume-guarantee. The rule a producer's proof asserts on its output is the rule the consumer's proof assumes on its input. A library parameter that switches input-side rules between assert and assume lets one checker serve both.
// snippet: symbolic ID tracking for the out-of-order responder (previous lesson)
// sym_id is an input of the formal harness; the tool may pick any value, then it stays put.
m_sym: assume property (@(posedge clk) $stable(sym_id));
logic tracked;
logic [7:0] exp_d;
always_ff @(posedge clk)
if (!rst_n) tracked <= 0;
else if (req_valid && req_id == sym_id) begin tracked <= 1; exp_d <= req_data + 8'h11; end
else if (rsp_valid && rsp_id == sym_id) tracked <= 0;
a_sym: assert property (@(posedge clk) disable iff (!rst_n)
rsp_valid && rsp_id == sym_id |-> tracked && rsp_data == exp_d);The bench checks a round-robin arbiter with the parts of the harness Verilator runs: the assumption (checked, as the LRM requires for simulation), two safety assertions and the bounded-wait assertion. The stimulus obeys the assumption by keeping each pending request until it is granted, which is what a formal tool would do automatically.
// 4-way round-robin arbiter: one grant per cycle, rotating priority.
module rr_arb (
input logic clk,
input logic rst_n,
input logic [3:0] req,
output logic [3:0] gnt
);
logic [1:0] last;
always_comb begin
gnt = '0;
for (int k = 1; k <= 4; k++) begin
automatic logic [1:0] idx = 2'(last + k);
if (gnt == '0 && req[idx]) gnt[idx] = 1'b1;
end
end
always_ff @(posedge clk) begin
if (!rst_n) last <= 2'd3;
else
for (int i = 0; i < 4; i++) if (gnt[i]) last <= 2'(i);
end
endmodule
Two mutants. Replace the rotating priority with fixed priority (idx = 2'(k - 1)): requesters 2 and 3 wait too long and the bounded-wait assertion fails 2196 times. Change the stimulus so requester 2 drops a pending request: the assumption reports 132 violations, which is exactly the check that keeps a simulation environment honest about what the formal proof assumes.
a_wait with no failures, but you wrote m_hold as req[i] |=> req[i]. What is wrong?That assumption forces a request to stay high forever once raised, even after it is granted. It over-constrains the environment, so the proof covers only traces where requests never drop. Covers on a request being dropped after a grant would show the space is too small.
req |-> s_eventually gnt not a substitute for req |-> ##[1:3] gnt in simulation?s_eventually is a liveness property with no deadline. On a finite trace it cannot be shown to fail at a specific cycle, and what a simulator reports for an attempt still pending at the end is tool-dependent. The bounded form fails at a known cycle.