Implication and vacuous success
Write properties as "if this happens, then that must follow", choose between |-> and |=>, and tell a real pass from a vacuous one.
- Lesson
- A1.3
- Time
- 25 min
- Runs on
- Verilator
- Check
- 5 questions
A team's assertion dashboard showed the error-response rule passing 18,000 times per test: after a transfer with rresp set to SLVERR, rvalid must drop. Months later, a bug in that exact path reached silicon. The tests had never produced a single error response, so every one of those passes was vacuous. The condition that starts the check never happened, and the assertion passed without checking anything.
Every edge starts an attempt, and every attempt passes vacuously because no error response ever occurs.
Antecedent, implication, consequent
Most properties have the form antecedent |-> consequent. The antecedent is the trigger, a condition or sequence that starts the check. The consequent is what must then hold.
At every clock edge, the assertion starts a new attempt. If the antecedent matches at that edge, the attempt checks the consequent and passes or fails. If the antecedent does not match, the attempt ends at once with a vacuous success.
Attempts overlap. A rule with a three-cycle consequent can have three attempts in progress at once, each started by a different edge and each with its own result.
Overlapped and non-overlapped
The overlapped implication |-> starts the consequent at the same edge where the antecedent ends. The non-overlapped implication |=> starts it one edge later, and a |=> b means exactly a |-> ##1 b.
| Property | Antecedent true at edge 3: consequent checked at |
|---|---|
a |-> b | edge 3 |
a |=> b | edge 4 |
a |-> ##2 b | edge 5 |
Where each form checks its consequent.
Choose from the spec, not from what makes a test pass. The rule "while rlast is high, rvalid is high" is about one edge, so it uses |->. The rule "a stalled offer is still there at the next edge" relates two edges, so it uses |=>.
Vacuous success
A vacuous success is still a success. The assertion does not fail, and its pass action, if it has one, runs as for a real pass, which is how the dashboard in the opening story counted 18,000 passes.
req |=> gnt. Only the attempts where req is high check anything.- Edge 1 · t=5
reqis low, so the attempt from 5 ends at once with a vacuous success. Its pass action still runs. - Edge 2 · t=15
reqis high at 15. This attempt is real, and it waits for the next edge to checkgnt. - Edge 3 · t=25
gntis high, so the attempt from 15 passes. The attempt that starts at 25 is vacuous, even thoughgntis high: no request asked for it. - Edge 4 · t=35
Another vacuous success.
gntis still high, and still nothing checks it. - Edge 5 · t=45
A second request starts a real attempt, which will look at
gntat 55. - Edge 6 · t=55
gntis low, so the attempt from 45 fails. The attempt from 55 is vacuous.More context
The pass action ran five times, but only one of those passes checked
gnt. Counting the edges wherereqwas high gives the real number of checks: two.
Read the lanes as text
- attempt @5, from edge 1 (t=5): succeeds vacuously at edge 1 (t=5).
- attempt @15, from edge 2 (t=15): passes at edge 3 (t=25).
- attempt @25, from edge 3 (t=25): succeeds vacuously at edge 3 (t=25).
- attempt @35, from edge 4 (t=35): succeeds vacuously at edge 4 (t=35).
- attempt @45, from edge 5 (t=45): fails at edge 6 (t=55).
- attempt @55, from edge 6 (t=55): succeeds vacuously at edge 6 (t=55).
Sampled values, edge 1 to 6: req 0 1 0 0 1 0; gnt 0 0 1 1 0 0.
The program behind the lanes
The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of gnt_next that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic req = 0, gnt = 0;
gnt_next: assert property (@(posedge clk) req |=> gnt);
initial begin
@(negedge clk); req = 1; // request sampled at 15
@(negedge clk); req = 0; gnt = 1; // grant sampled at 25 and 35
@(negedge clk);
@(negedge clk); req = 1; gnt = 0; // request sampled at 45
@(negedge clk); req = 0; // no grant at 55
@(negedge clk);
$finish;
end
endmodule
A vacuous success ends at the edge where it starts. A real attempt spans to the edge where its consequent is checked.
So a pass count says nothing about whether a rule was exercised. What matters is how often the antecedent matched. Count that separately, with a small counter or with a cover property on the antecedent, and treat a count of zero as a hole in the tests.
Counting the real checks
A few lines beside the assertion show how often a rule was really exercised: always @(posedge clk) if (rvalid && rready && rresp == 2) n_err++;. Print the count at the end of the test, or fail the test when it is zero.
Report the count next to the assertion's name in the regression summary. A rule with thousands of passes and zero real checks then stands out at a glance, instead of hiding behind a green status.
Longer antecedents
An antecedent can be a short sequence rather than a single condition. rvalid && !rready ##1 rvalid && !rready |-> ... triggers only on the second edge of a stall, for a rule about long stalls.
Keep antecedents short and of fixed length where you can. A long or open-ended antecedent makes it hard to see which edges started an attempt, and harder still to count real checks.
A rule with two triggers in a row, "after a, if b follows, then c", is clearest as one sequence: a ##1 b |=> c. The nested form a |=> b |=> c means the same by the standard, but it is not covered yet. In a measured run, Verilator 5.052 missed its failure, while the flat form reported it at 85.
Designing the antecedent
An antecedent that is too wide makes a rule fail on legal traffic. rvalid |=> rvalid says a valid offer is always followed by another one, which is false after the last beat of a burst. rvalid && !rready |=> rvalid triggers only on a stall, which is what the rule is about.
An antecedent that is too narrow makes a rule check almost nothing. An antecedent that requires a rare combination, such as an error response on the last beat of a 16-beat burst, may never match in a whole regression.
No implication
assert property (@(posedge clk)
rvalid ##1 rready);
Demands the whole sequence from every edge, so it fails wherever rvalid is low.
Implication
assert property (@(posedge clk)
rvalid |=> rready);
Checks the rule only where rvalid is high, and passes vacuously elsewhere.
Without an implication, a property must hold from every single edge. That is right for invariants such as !(full && empty), and wrong for any rule that starts with "when" or "after".
Try it
The error-response rule passed at all 7 edges, yet its antecedent never matched: every pass was vacuous. The sequence written without an implication failed at every edge where rvalid was low, including legal idle cycles.
Add one error transfer to the stimulus by setting rresp = 2; at the falling edge before a transfer, and predict all three counts.
err_ends: 7 passes, 0 fails, 0 real checks no_impl: 4 failsCheck yourself
Remember
- Every clock edge starts an attempt; a false antecedent ends it with a vacuous success.
|->checks the consequent at the edge where the antecedent ends;|=>checks it one edge later.- Vacuous successes count as passes, so count antecedent matches to know a rule was exercised.
- Make the antecedent match exactly the situation the rule is about.
- Without an implication, a property must hold from every edge: right for invariants only.