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.

Passes with nothing checked
01234edge12345rvalid && rreadyrrespOKAYerr_ends resultvacuousantecedent false: vacuous passstill no real check

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.

PropertyAntecedent true at edge 3: consequent checked at
a |-> bedge 3
a |=> bedge 4
a |-> ##2 bedge 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.

Six attempts, two real checksEvery edge starts an attempt of req |=> gnt. Only the attempts where req is high check anything.req |=> gntMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfailvacuous success: the antecedent did not match
Six attempts, two real checksattempt @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).edgetime15215325435545655clkreqgntattemptsattempt @5attempt @5: succeeds vacuously at edge 1 (t=5)attempt @15attempt @15: passes at edge 3 (t=25)attempt @25attempt @25: succeeds vacuously at edge 3 (t=25)attempt @35attempt @35: succeeds vacuously at edge 4 (t=35)attempt @45attempt @45: fails at edge 6 (t=55)attempt @55attempt @55: succeeds vacuously at edge 6 (t=55)
  1. Edge 1 · t=5

    req is low, so the attempt from 5 ends at once with a vacuous success. Its pass action still runs.

  2. Edge 2 · t=15

    req is high at 15. This attempt is real, and it waits for the next edge to check gnt.

  3. Edge 3 · t=25

    gnt is high, so the attempt from 15 passes. The attempt that starts at 25 is vacuous, even though gnt is high: no request asked for it.

  4. Edge 4 · t=35

    Another vacuous success. gnt is still high, and still nothing checks it.

  5. Edge 5 · t=45

    A second request starts a real attempt, which will look at gnt at 55.

  6. Edge 6 · t=55

    gnt is 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 where req was 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.

RUNVerilator · simulator service
Edit the code, then run it
Expected: err_ends: 7 passes, 0 fails, 0 real checks no_impl: 4 fails

Check yourself

Predict the outputa is never 1. The run stops at t = 40, after rising edges at 5, 15, 25 and 35. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, 35
  logic a = 0, b = 0;
  int passes, fails;
  p: assert property (@(posedge clk) a |-> b) passes++; else fails++;
  initial begin
    #40 $display("passes=%0d fails=%0d", passes, fails);
    $finish;
  end
endmodule
ConceptWhich property means exactly the same as a |=> b?
ConceptAn assertion reports 50,000 passes and no failures. What do you know about the rule it checks?
Read the waveformp: assert property (@(posedge clk) req |=> gnt); Which edges give a real (non-vacuous) result, and what is it?
Sampled values at each rising edge
01234edge12345reqgnt

Each edge starts one attempt.

Spot the bugEach option is the property for "after a transfer with rresp = 2, rvalid is low at the next edge". Which option fails on legal traffic?

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.