Weak and strong: what the end of a test means

Know what happens to attempts still running when a test stops, tell weak properties from strong ones, and end tests so that both report only real bugs.

Lesson
A3.1
Time
25 min
Runs on
Verilator
Check
5 questions

One read in about ten thousand never got a response: the DUT dropped it under a rare arbitration case. The rule was arvalid |-> ##[1:$] rvalid, and every test passed. The dropped read was still an open attempt when the test stopped, and an open attempt of a weak property is not a failure.

A request that is never answered
0123456edge123456endarvalidrvalid##[1:$] rvalidwaitnones_eventually rvalidwaitFAILattempt startstest stops: weak gives no verdict, strong fails

Both rules wait for the same answer. Only the strong one turns "still waiting" into a failure.

Attempts still running at the end

A safety rule, such as "rlast never rises without rvalid", fails at a definite edge. A liveness rule, such as "every read is answered", fails only if the answer never comes. A simulation always stops after a finite time, so that moment never arrives.

When $finish runs, some attempts are still waiting. Each property has a strength that decides their fate. A weak attempt that is still waiting ends with no verdict. A strong attempt that is still waiting fails at the end of the run.

FormStrengthStill waiting when the test stops
a plain sequence as the consequent: ##[1:4] ack, ##[1:$] ackweakno verdict, no failure
p until q, p until_with qweakno verdict, no failure
s_eventually ackstrongfails at the end of the run
p s_until q, p s_until_with qstrongfails at the end of the run

Under assert, a sequence used as a property is weak by default. The s_ operators are strong.

The first row holds the surprise. The weakness comes from that default, not from any operator, so a bounded window is weak too. If the test stops while the window is still open, that attempt never fails, however close it was to its deadline.

Pending at the end

Both figures check the same traffic: one request answered two edges later, one request the DUT drops, and one legal request still in flight when the test stops at 120.

A bounded rule when the test stopsOne lane per request for req |-> ##[1:4] ack. Each delay of the range is a thread.req |-> ##[1:4] ackMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfailstill running at the end: weak, no failurethread dies; the attempt goes onthread dropped: the attempt has already passed
A bounded rule when the test stopsreq @15, from edge 2 (t=15): thread 1 (to edge 3 (t=25)) dies at edge 3 (t=25); thread 2 (to edge 4 (t=35)) passes at edge 4 (t=35); thread 3 (to edge 5 (t=45)) is dropped at edge 4 (t=35), because the attempt has already passed; thread 4 (to edge 6 (t=55)) is dropped at edge 4 (t=35), because the attempt has already passed. req @45, from edge 5 (t=45): thread 1 (to edge 6 (t=55)) dies at edge 6 (t=55); thread 2 (to edge 7 (t=65)) dies at edge 7 (t=65); thread 3 (to edge 8 (t=75)) dies at edge 8 (t=75); thread 4 (to edge 9 (t=85)) fails at edge 9 (t=85). req @105, from edge 11 (t=105): thread 1 (to edge 12 (t=115)) dies at edge 12 (t=115); thread 2 (to the end of the run (t=120)) is still running at the end of the run: weak, so no failure; thread 3 (to the end of the run (t=120)) is still running at the end of the run: weak, so no failure; thread 4 (to the end of the run (t=120)) is still running at the end of the run: weak, so no failure.edgetime1521532543554565576587598510951110512115end120clkreqackattemptsreq @15req @15: dies at edge 3 (t=25)req @15: passes at edge 4 (t=35)req @45req @45: dies at edge 6 (t=55)req @45: dies at edge 7 (t=65)req @45: dies at edge 8 (t=75)req @45: fails at edge 9 (t=85)req @105req @105: dies at edge 12 (t=115)req @105: is still running at the end of the run: weak, so no failurereq @105: is still running at the end of the run: weak, so no failurereq @105: is still running at the end of the run: weak, so no failure
  1. Edge 2 · t=15

    The request at 15 forks four threads, one per delay of ##[1:4].

  2. Edge 4 · t=35

    ack at 35 ends the attempt with a pass. The longer threads stop looking.

  3. Edge 5 · t=45

    The DUT drops the request at 45. Its four threads start waiting.

  4. Edge 9 · t=85

    At 85 the last thread runs out of window, so the dropped request fails here.

  5. Edge 11 · t=105

    A legal request at 105. Its answer is due at 125.

  6. End of the run · t=120

    The test stops at 120 with three threads still open. The consequent is weak, so this request ends with no verdict: it was never checked.

Read the lanes as text
  • req @15, from edge 2 (t=15): thread 1 (to edge 3 (t=25)) dies at edge 3 (t=25); thread 2 (to edge 4 (t=35)) passes at edge 4 (t=35); thread 3 (to edge 5 (t=45)) is dropped at edge 4 (t=35), because the attempt has already passed; thread 4 (to edge 6 (t=55)) is dropped at edge 4 (t=35), because the attempt has already passed.
  • req @45, from edge 5 (t=45): thread 1 (to edge 6 (t=55)) dies at edge 6 (t=55); thread 2 (to edge 7 (t=65)) dies at edge 7 (t=65); thread 3 (to edge 8 (t=75)) dies at edge 8 (t=75); thread 4 (to edge 9 (t=85)) fails at edge 9 (t=85).
  • req @105, from edge 11 (t=105): thread 1 (to edge 12 (t=115)) dies at edge 12 (t=115); thread 2 (to the end of the run (t=120)) is still running at the end of the run: weak, so no failure; thread 3 (to the end of the run (t=120)) is still running at the end of the run: weak, so no failure; thread 4 (to the end of the run (t=120)) is still running at the end of the run: weak, so no failure.

Sampled values, edge 1 to 12: req 0 1 0 0 1 0 0 0 0 0 1 0; ack 0 0 0 1 0 0 0 0 0 0 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 ack_within_4 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, ack = 0;

  ack_within_4: assert property (@(posedge clk) req |-> ##[1:4] ack);

  initial begin
    @(negedge clk); req = 1;            // request at 15
    @(negedge clk); req = 0;
    @(negedge clk); ack = 1;            // answered at 35
    @(negedge clk); ack = 0; req = 1;   // request at 45: the DUT drops it
    @(negedge clk); req = 0;
    repeat (5) @(negedge clk);
    req = 1;                            // request at 105, legal, answer due at 125
    @(negedge clk); req = 0;
    @(negedge clk);
    $finish;                            // the test stops at 120
  end
endmodule
A strong rule on the same trafficOne lane per request for req |-> s_eventually ack.req |-> s_eventually ackMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassstill running at the end: strong, fails
A strong rule on the same trafficreq @15, from edge 2 (t=15): passes at edge 4 (t=35). req @45, from edge 5 (t=45): is still running at the end of the run: strong, so it fails. req @105, from edge 11 (t=105): is still running at the end of the run: strong, so it fails.edgetime1521532543554565576587598510951110512115end120clkreqackattemptsreq @15req @15: passes at edge 4 (t=35)req @45req @45: is still running at the end of the run: strong, so it failsreq @105req @105: is still running at the end of the run: strong, so it fails
  1. Edge 2 · t=15

    The request at 15 starts waiting for ack, with no deadline.

  2. Edge 4 · t=35

    It passes at 35.

  3. Edge 5 · t=45

    The dropped request starts waiting. Nothing in this run will end the wait.

  4. Edge 11 · t=105

    The legal request at 105 starts waiting too.

  5. End of the run · t=120

    Both waits are still open at 120, and both fail there. A strong rule cannot tell a lost request from one in flight.

Read the lanes as text
  • req @15, from edge 2 (t=15): passes at edge 4 (t=35).
  • req @45, from edge 5 (t=45): is still running at the end of the run: strong, so it fails.
  • req @105, from edge 11 (t=105): is still running at the end of the run: strong, so it fails.

Sampled values, edge 1 to 12: req 0 1 0 0 1 0 0 0 0 0 1 0; ack 0 0 0 1 0 0 0 0 0 0 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 ack_some_time 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, ack = 0;

  ack_some_time: assert property (@(posedge clk) req |-> s_eventually ack);

  initial begin
    @(negedge clk); req = 1;            // request at 15
    @(negedge clk); req = 0;
    @(negedge clk); ack = 1;            // answered at 35
    @(negedge clk); ack = 0; req = 1;   // request at 45: the DUT drops it
    @(negedge clk); req = 0;
    repeat (5) @(negedge clk);
    req = 1;                            // request at 105, legal, answer due at 125
    @(negedge clk); req = 0;
    @(negedge clk);
    $finish;                            // the test stops at 120
  end
endmodule

Read the two figures together. The bounded rule caught the dropped request at a definite edge, but skipped the last one without a word. The strong rule caught both, and blamed a legal request as well. Neither is trustworthy until the test stops cleanly.

Weak with no deadline checks nothing

In arvalid |-> ##[1:$] rvalid the consequent is a plain sequence, so it is weak, and $ takes away the deadline. A dropped request waits forever, so in simulation its attempt can never fail. In the example below it stays silent where both other rules report.

Treat such a rule in a simulation suite as a check that cannot fire. Replace it with a bound, or with s_eventually and a clean end of test.

Bounds are what simulation can use

A bound turns liveness into safety. arvalid |-> ##[1:16] rvalid says that no read waits more than 16 edges. It fails at a definite edge, close to the bug, and the number records a real requirement.

Take the bound from the specification or from the design's worst case, then add a margin. A bound that is too tight fails on legal traffic. One that is too loose, such as 1,000,000, behaves like ##[1:$] in a short test.

No deadline, weak

assert property (@(posedge clk)
  arvalid |-> ##[1:$] rvalid);

A dropped read passes, because the run ends while the attempt is still waiting.

Bounded

assert property (@(posedge clk)
  arvalid |-> ##[1:16] rvalid);

A dropped read fails 16 edges later, at a time that points at it.

Choosing a strength

Each form has its own job. A bound states a latency requirement and reports at a definite edge, so it is the main check in simulation. s_eventually states that something must happen when no limit is given, and it only makes sense in a test that drains. A weak consequent with no deadline states nothing that a simulation can refute.

Strong rules also help when a test hangs. If a watchdog stops a stuck test with $finish, every strong attempt still open fails at that moment, and each failure names a request that never completed. That is a far better starting point for debugging than a bare timeout message.

Ending a test cleanly

Both kinds of rule need the same thing from the test. Stop new traffic, wait until every outstanding request has been answered, then call $finish. After this drain, every bounded window has closed, and a strong failure at the end can only mean a lost request.

Drain on state rather than on a guess: wait until the scoreboard has nothing outstanding, with a timeout of the longest bound. Then check that count at the end of the test, as Module 2 did. The assertions and the scoreboard agree when everything is correct, and each catches mistakes in the other.

Explicit strong(...) and weak(...) wrappers are not covered yet.

Try it

All the traffic was legal. The test stopped at 80 while the request from 75 was still waiting for its answer, so the strong rule failed. The bounded rule stayed silent because its open window is weak, and the rule with no deadline cannot fail at all.

Add a drain before the $display: raise ack at the next falling edge, clear it, wait two more edges, and predict the output.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=80 test ended t=80 strong_eventually failed

Check yourself

Predict the outputThe same test now drains: the answer for the request at 75 arrives at 95, and the test stops at 120. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                 // rising edges at 5, 15, 25, ...
  logic req = 0, ack = 0;

  // Three ways to say "every request is answered".
  bounded:           assert property (@(posedge clk) req |-> ##[1:4] ack)
    else $display("t=%0t bounded failed", $time);
  strong_eventually: assert property (@(posedge clk) req |-> s_eventually ack)
    else $display("t=%0t strong_eventually failed", $time);
  weak_unbounded:    assert property (@(posedge clk) req |-> ##[1:$] ack)
    else $display("t=%0t weak_unbounded failed", $time);

  initial begin                         // legal traffic: each answer two edges later
    @(negedge clk); req = 1;            // request at 15
    @(negedge clk); req = 0;
    @(negedge clk); ack = 1;            // answered at 35
    @(negedge clk); ack = 0; req = 1;   // request at 45
    @(negedge clk); req = 0;
    @(negedge clk); ack = 1;            // answered at 65
    @(negedge clk); ack = 0; req = 1;   // request at 75, answer due at 95
    @(negedge clk); req = 0;
    @(negedge clk); ack = 1;            // answered at 95
    @(negedge clk); ack = 0;
    repeat (2) @(negedge clk);          // drain
    $display("t=%0t test ended", $time);
    $finish;
  end
endmodule
ConceptA test stops while an attempt of req |-> ##[1:8] ack is three edges into its window. What is that attempt's result?
ConceptThe spec says a read is answered within 40 cycles. Which property is the most useful in simulation?
Read the waveformarvalid |-> ##[1:3] rvalid checks this trace. At which edge does the attempt from edge 2 fail?
Two requests
0123456edge1234567arvalidrvalid

One response, at edge 2.

Spot the bugEach option is the property for "every request is answered". The test includes one request that is never answered and drains before it stops. Which option lets the lost request pass?

Remember

  • An attempt still waiting at $finish gets no verdict if the property is weak, and fails if it is strong.
  • Under assert, a plain sequence is weak by default, so a bounded window cut off by the end of the test is never checked.
  • A weak consequent with no deadline, such as ##[1:$] ack, cannot fail in simulation.
  • A strong rule at the end of the run cannot tell a lost request from one in flight.
  • Drain every test, then both bounded and strong rules report only real losses.

Practice