An attempt whose antecedent never matches is a vacuous pass. The obligation never activated, so nothing was violated and nothing was checked.
The risk is statistical. Thousands of passes can pile up while the consequent logic is never examined once.
Slot 0: req 0. Attempt starts, antecedent misses, vacuous pass.
Slot 1: req 0. Same outcome, second vacuous pass.
Slot 2: req 0. Same outcome, third vacuous pass.
Slot 3: req 0. Same outcome, fourth vacuous pass.
Slot 4: req 0. Same outcome, fifth vacuous pass.
Slot 5: req 0. Same outcome, sixth vacuous pass.
Six attempts, six vacuous passes, zero examinations of gnt. The tool log looks identical to a healthy run.
Every implication ships with a cover of its antecedent sequence. The assert proves never violated, and the cover proves actually exercised.
Cover the sequence, never the implication. Covering ante |-> cons is itself satisfied by vacuous passes.
// snippet
a_gnt: assert property (@(posedge clk) disable iff (!rst_n)
$rose(req) |-> ##[1:2] gnt);
c_ante: cover property (@(posedge clk) disable iff (!rst_n)
$rose(req));
c_full: cover property (@(posedge clk) disable iff (!rst_n)
$rose(req) ##[1:2] gnt);Cover hit | Cover not hit | |
|---|---|---|
Assert passes | Checked and clean | Vacuous: fix the stimulus, not the logic |
Assert fails | Real bug or bad assertion | Impossible: a nonvacuous failure proves the antecedent fired |
Simulators separate real and vacuous passes in assertion reports. Consult the vendor docs for the exact options, since flag names differ across tools.
Keep the sign-off rule in the regression script. Fail the run when an assertion passes but its paired cover has zero hits across the merged regression.
cover property (a |-> b) the wrong vacuity guard?Vacuous attempts satisfy the implication, so the cover hits without any real activation. Cover the antecedent sequence instead.
In the regression script, on the merged regression. It fails the run when an assert passes while its paired cover has zero hits.
A never-firing antecedent plus the paired cover that proves the passes are vacuous. The bench reads a procedural mirror of the cover count, then fires two real requests to show the assertion is live: one granted, one not.
module tb_impl_vac;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
logic req = 0;
logic gnt = 0;
logic req_q = 0;
int ante_hits = 0;
int fails = 0;
a_vac: assert property (@(posedge clk) disable iff (!rst_n) req |-> gnt)
else fails++;
c_ante: cover property (@(posedge clk) disable iff (!rst_n) $rose(req));
// Procedural mirror of c_ante, so the bench can read the hit count.
always @(posedge clk) begin
req_q <= req;
if (rst_n && req && !req_q) ante_hits <= ante_hits + 1;
end
initial begin
int vac_hits, vac_fails;
repeat (2) @(negedge clk);
rst_n = 1;
repeat (6) @(negedge clk); // phase 1: req never rises
vac_hits = ante_hits;
vac_fails = fails;
req = 1; gnt = 1; // phase 2: a granted request
@(negedge clk);
req = 0; gnt = 0;
@(negedge clk);
req = 1; // phase 3: a request with no grant
@(negedge clk);
req = 0;
repeat (2) @(negedge clk);
if (vac_hits == 0 && vac_fails == 0 && ante_hits == 2 && fails == 1)
$display("PASS: 6 vacuous cycles: 0 cover hits, 0 failures; then 2 real requests: 2 hits, 1 failure");
else
$fatal(1, "FAIL: vac_hits=%0d vac_fails=%0d ante_hits=%0d fails=%0d", vac_hits, vac_fails, ante_hits, fails);
$finish;
end
endmodulePASS: 6 vacuous cycles: 0 cover hits, 0 failures; then 2 real requests: 2 hits, 1 failureRun with Verilator 5.048 (--binary --timing --assert, plus --coverage when a listing has a cover property) on 2026-10-18; every bench printed the PASS line shown. Verilator stops at the first $error or failing assertion without an action block, so the benches count failures in the action block. Icarus 13 has no concurrent assertions.