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.
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.
| Form | Strength | Still waiting when the test stops |
|---|---|---|
a plain sequence as the consequent: ##[1:4] ack, ##[1:$] ack | weak | no verdict, no failure |
p until q, p until_with q | weak | no verdict, no failure |
s_eventually ack | strong | fails at the end of the run |
p s_until q, p s_until_with q | strong | fails 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.
req |-> ##[1:4] ack. Each delay of the range is a thread.- Edge 2 · t=15
The request at 15 forks four threads, one per delay of
##[1:4]. - Edge 4 · t=35
ackat 35 ends the attempt with a pass. The longer threads stop looking. - Edge 5 · t=45
The DUT drops the request at 45. Its four threads start waiting.
- Edge 9 · t=85
At 85 the last thread runs out of window, so the dropped request fails here.
- Edge 11 · t=105
A legal request at 105. Its answer is due at 125.
- 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
req |-> s_eventually ack.- Edge 2 · t=15
The request at 15 starts waiting for
ack, with no deadline. - Edge 4 · t=35
It passes at 35.
- Edge 5 · t=45
The dropped request starts waiting. Nothing in this run will end the wait.
- Edge 11 · t=105
The legal request at 105 starts waiting too.
- 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.
t=80 test ended t=80 strong_eventually failedCheck yourself
Remember
- An attempt still waiting at
$finishgets 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
- Answer in time Verify · Medium