SVA Examples: $past, $stable, disable iff and Repetition [*], [->], [=] (Pass and Fail)

In short: $stable(x) is true when x has the same value as at the previous clock edge. $past(x, N) gives the value x had N clock edges ago. disable iff (rst) cancels any check in progress while reset is high, so it neither passes nor fails. The repetition operators count: b[*3] means 3 cycles in a row, b[->3] means the 3rd time b is true (gaps allowed) with the next step right after it, and b[=3] means 3 times with gaps allowed and idle cycles allowed after the 3rd.

Part 1 covered the basics (##, implication, edges). See SVA with examples.

This part covers the operators you reach for next: history, stability, reset and counting.

Every example below has a stimulus you can read cycle by cycle, the real output from a simulator, and a waveform.

Think of it like a security guard with a camera

The guard doesn’t watch nonstop. The camera takes one snapshot per clock edge, and the guard only judges snapshots.

  • **$stable(x):** “Is the parcel still exactly where it was in the last snapshot?”
  • **$past(x, 2):** “Rewind two snapshots. What did it look like then?”
  • **disable iff (rst):** a fire drill. The guard drops every check in progress. Nobody gets written up for what happened during the drill.
  • **[*3]: “The door was open in 3 snapshots in a row**.”
  • **[->3]: “On the 3rd badge swipe, the door must open in the very next** snapshot.”
  • **[=3]: “After the 3rd swipe, the door may open later**, as long as nobody swipes a 4th time first.”

One detail makes all of this work: the snapshot is taken just before the clock edge. So a signal you change at the edge is seen at the next snapshot.

Example 1: $stable, data must hold while waiting

A classic handshake rule (AXI has the same one): once valid is up and ready is low, data must not change until the handshake.

// $stable: data must not change while valid is waiting for ready
module tb;
  logic clk = 1, valid = 0, ready = 0;
  logic [7:0] data = 0;
  always #5 clk = ~clk;              // rising edges at 10, 20, 30 ... = cycles 1, 2, 3 ...

  a_hold: assert property (@(posedge clk) (valid && !ready) |=> $stable(data))
    else $display("cycle %0t: FAIL a_hold, data went %0d -> %0d while waiting",
                  $time/10, $past(data), data);

  initial begin                      // each line sets the values seen at that cycle
    #5  valid = 1; data = 10;        // cycle 1: waiting
    #10 ;                            // cycle 2: waiting, data held
    #10 ready = 1;                   // cycle 3: handshake
    #10 valid = 0; ready = 0;        // cycle 4: idle
    #10 valid = 1; data = 20;        // cycle 5: waiting
    #10 data = 21;                   // cycle 6: waiting, data CHANGED (the bug)
    #10 ready = 1;                   // cycle 7: handshake
    #10 valid = 0; ready = 0;        // cycle 8: idle
    #20 $finish;
  end
endmodule

Output (Verilator 5.020 with --assert, 11 Nov 2026):

cycle 6: FAIL a_hold, data went 20 -> 21 while waiting
Cycle12345678
valid11101110
ready00100010
data1010101020212121
ResultpasspassFAILpass
  • Cycles 1 and 2: waiting, data held at 10. Fine.
  • Cycle 5 was waiting with data 20. At cycle 6, data is 21. One failure, at the exact cycle it happened.
  • |=> means “check at the next edge”, which is why the rule looks one cycle ahead.
Waveform: valid waits with ready low, data changes from 20 to 21 at cycle 6, and the $stable assertion fails at cycle 6
Figure 1. $stable: data changed while valid was waiting, so the check fails at cycle 6.

Example 2: $past, ack must come exactly 2 cycles after req

// $past: every ack must have a req exactly 2 cycles earlier
module tb;
  logic clk = 1, req = 0, ack = 0;
  always #5 clk = ~clk;              // cycle N = rising edge at time 10*N

  a_ack: assert property (@(posedge clk) ack |-> $past(req, 2))
    else $display("cycle %0t: FAIL a_ack, no req 2 cycles earlier", $time/10);

  initial begin
    #15 req = 1;                     // cycle 2: req
    #10 req = 0;                     // cycle 3
    #10 ack = 1;                     // cycle 4: ack, 2 cycles after req  -> good
    #10 ack = 0;                     // cycle 5
    #10 req = 1;                     // cycle 6: req
    #10 req = 0;                     // cycle 7
    #20 ack = 1;                     // cycle 9: ack, 3 cycles after req  -> bad
    #10 ack = 0;                     // cycle 10
    #20 $finish;
  end
endmodule

Output (Verilator 5.020 with --assert):

cycle 9: FAIL a_ack, no req 2 cycles earlier
  • The ack at cycle 4 looks back 2 snapshots, finds req at cycle 2. Pass.
  • The ack at cycle 9 looks back to cycle 7. No req there (it was at 6). Fail.
  • $past lets you write the rule from the effect back to the cause. That’s often easier than starting from req and looking forward.
Waveform: ack at cycle 4 finds req two cycles back at cycle 2 and passes; ack at cycle 9 looks back to cycle 7, finds no req and fails
Figure 2. $past(req, 2): every ack looks back two cycles.

Example 3: disable iff, the same rule with and without a reset guard

// disable iff: the same rule, with and without a reset guard
module tb;
  logic clk = 1, rst = 0, valid = 0, ready = 0;
  logic [7:0] data = 0;
  always #5 clk = ~clk;              // cycle N = rising edge at time 10*N

  a_no_guard: assert property (@(posedge clk)
                (valid && !ready) |=> $stable(data))
    else $display("cycle %0t: FAIL a_no_guard", $time/10);

  a_guarded:  assert property (@(posedge clk) disable iff (rst)
                (valid && !ready) |=> $stable(data))
    else $display("cycle %0t: FAIL a_guarded", $time/10);

  initial begin
    #5  valid = 1; data = 10;        // cycle 1: waiting
    #10 ;                            // cycle 2: still waiting
    #10 rst = 1; valid = 0; data = 0;// cycle 3: reset clears everything
    #10 rst = 0;                     // cycle 4
    #20 $finish;
  end
endmodule

Output (Verilator 5.020 with --assert):

cycle 3: FAIL a_no_guard
  • Cycle 2 started a check: “data must still be 10 at cycle 3”.
  • At cycle 3, reset cleared data to 0. Without a guard, that’s a failure: a false alarm, because a reset is allowed to clear data.
  • a_guarded saw rst high and dropped the check. No pass, no fail. That’s the fire drill.
Waveform: reset at cycle 3 clears data while valid was waiting; the unguarded assertion fails at cycle 3, the disable iff version drops the check
Figure 3. disable iff turns a reset false alarm into a dropped check.

Rule of thumb: almost every concurrent assertion in a real design wants disable iff (rst). Without it, the first reset in every test can raise a failure that isn’t a bug.

Example 4: [*3], three cycles in a row

// [*3]: busy must be high for exactly 3 cycles in a row, then done
module tb;
  logic clk = 1, start = 0, busy = 0, done = 0;
  always #5 clk = ~clk;              // cycle N = rising edge at time 10*N

  a_busy3: assert property (@(posedge clk) $rose(start) |=> busy[*3] ##1 done)
    else $display("cycle %0t: FAIL a_busy3", $time/10);

  initial begin
    // attempt 1: start at 1, busy 2-4, done 5        -> pass
    #5  start = 1;                   // cycle 1
    #10 start = 0; busy = 1;         // cycle 2
    #20 ;                            // cycles 3, 4 busy
    #10 busy = 0; done = 1;          // cycle 5
    #10 done = 0;                    // cycle 6
    // attempt 2: start at 8, busy 9-10, gap at 11    -> fail at 11
    #20 start = 1;                   // cycle 8
    #10 start = 0; busy = 1;         // cycle 9
    #10 ;                            // cycle 10
    #10 busy = 0;                    // cycle 11: busy dropped early
    #10 busy = 1;                    // cycle 12
    #10 busy = 0; done = 1;          // cycle 13
    #10 done = 0;
    #20 $finish;
  end
endmodule

Output (Synopsys VCS 2025.06, 11 Nov 2026):

"testbench.sv", 6: tb.a_busy3: started at 80ns failed at 110ns
	Offending 'busy'
cycle 11: FAIL a_busy3

VCS adds its own line on top of ours: the attempt started at 80 ns (cycle 8, the rise of start) and failed at 110 ns (cycle 11), and it names the signal that broke the rule: busy.

  • Attempt 1: busy at 2, 3, 4, then done at 5. Three in a row. Pass.
  • Attempt 2: busy at 9 and 10, then a gap at 11. [*3] needs them back to back, so the check fails the moment the run breaks, not later at done.
  • Picture it: the door must show open in 3 snapshots in a row. One closed snapshot in the middle and the rule fails right there.
Waveform: busy high for cycles 2 to 4 then done passes; busy high for 9 and 10 then low at 11 fails at cycle 11
Figure 4. busy[*3]: pass at cycle 5, fail at cycle 11 (VCS 2025.06).

Example 5: [->3] vs [=3] on the same signals

This is the pair everyone mixes up, so here they are side by side, watching the same wires.

// [->3] vs [=3]: same stimulus, two rules
module tb;
  logic clk = 1, req = 0, gnt = 0, enable = 0;
  always #5 clk = ~clk;              // cycle N = rising edge at time 10*N

  a_goto:  assert property (@(posedge clk) $rose(req) |-> gnt[->3] ##1 enable)
    else $display("cycle %0t: FAIL a_goto  ([->3])", $time/10);

  a_nonc:  assert property (@(posedge clk) $rose(req) |-> gnt[=3]  ##1 enable)
    else $display("cycle %0t: FAIL a_nonc  ([=3])", $time/10);

  task automatic pulse_gnt(); gnt = 1; #10 gnt = 0; endtask

  initial begin
    // attempt 1: req at 1, gnt at 3, 5, 7, enable at 8           -> both pass
    #5  req = 1;                     // cycle 1
    #10 req = 0;                     // cycle 2
    #10 pulse_gnt();                 // cycle 3
    #10 pulse_gnt();                 // cycle 5
    #10 pulse_gnt();                 // cycle 7
    enable = 1; #10 enable = 0;      // cycle 8
    // attempt 2: req at 12, gnt at 14, 16, 18, enable at 21 (2 idle cycles)
    #30 req = 1;                     // cycle 12
    #10 req = 0;                     // cycle 13
    #10 pulse_gnt();                 // cycle 14
    #10 pulse_gnt();                 // cycle 16
    #10 pulse_gnt();                 // cycle 18
    #20 enable = 1;                  // cycle 21
    #10 enable = 0;
    #30 $finish;
  end
endmodule

Output (Synopsys VCS 2025.06, 11 Nov 2026):

"testbench.sv", 6: tb.a_goto: started at 120ns failed at 190ns
	Offending 'enable'
cycle 19: FAIL a_goto  ([->3])

Only a_goto failed, at cycle 19, and VCS points at enable. a_nonc printed nothing, so it passed both attempts. Attempt 1 passed for both rules.

Attempt 1: enable right after 3rd gntAttempt 2: enable 3 cycles after 3rd gnt
gnt[->3] ##1 enablePassFail at cycle 19
gnt[=3] ##1 enablePassPass at cycle 21
  • **[->3] (goto): the count ends on** the 3rd gnt. So ##1 enable means “enable in the very next cycle”. At cycle 19 it wasn’t there.
  • **[=3] (non-consecutive): the count can stretch past** the 3rd gnt over idle cycles. So enable at cycle 21 is fine, as long as gnt doesn’t come a 4th time first.
  • Badge picture: goto says “the door opens on the next snapshot after the 3rd swipe”. Non-consecutive says “the door opens some time after the 3rd swipe, before anyone swipes a 4th time”.
Waveform: two attempts; enable right after the 3rd gnt passes both rules; enable 3 cycles later fails the goto rule at 19 and passes the non-consecutive rule at 21
Figure 5. [->3] vs [=3]: same wires, goto fails at 19, non-consecutive passes at 21 (VCS 2025.06).

Quick reference

OperatorMeansPicture it as
$stable(x)Same value as last edgeParcel hasn’t moved since the last snapshot
$past(x, N)Value N edges agoRewind N snapshots
disable iff (c)Drop checks while c is trueFire drill: no pass, no fail
b[*N]b true N cycles in a rowOpen in N snapshots in a row
b[->N]N-th time b is true, ends on itDoor opens on the snapshot right after the N-th swipe
b[=N]N times b is true, idle cycles allowed afterDoor opens later, before a 4th swipe

Common mistakes (and the fix for each)

These come from real Verification Academy threads, retold in my own words with links.

1. $stable failing on the first clock

  • What happened: in 2016 an engineer wrote assert property (@(posedge clk) $stable(d)); and it failed on the very first edge (Verification Academy, 2016). The same question came back in 2020 for a 12-bit bus (Verification Academy, 2020).
  • Why: at the first edge there is no real “last snapshot”. $stable compares against the starting value, which may not match.
  • Fix: don’t check stability on its own. Give it a reason to start, like example 1’s (valid && !ready) |=>, or skip the first cycle with ##1 $stable(d).

2. Treating [->] and [=] as the same

  • What happened: in 2017 an engineer asked what the difference is, since both seemed to behave the same (Verification Academy, 2017).
  • Why: they only differ in where the count ends. With [->3], the next step must come right after the 3rd match. With [=3], idle cycles may sit in between.
  • Fix: run example 5. Same signals, one passes, one fails.

3. Reading a[=3] ##1 b as “b any time after”

  • What happened: in 2021 someone read a[=3] ##1 b as “b can come any time after the 3rd a”, and asked if ##5 would mean the same (Verification Academy, 2021).
  • Why: close, but a must stay low in the gap. A 4th a before b breaks the match. And a[=3] ##5 b is different again: it allows a to go high during those 5 cycles.
  • Fix: say it out loud: “3 a’s, then quiet, then b”.

4. Using a gated clock instead of disable iff

  • What happened: an engineer wrote @(posedge test_clk iff rstb) and expected the check to stop when rstb dropped. It kept going and failed after reset (Verification Academy).
  • Why: iff on the clock only skips snapshots. It doesn’t cancel checks already running. disable iff does.
  • Fix: @(posedge test_clk) disable iff (!rstb) ..., like example 3.

5. Failures from X values right after reset

  • What happened: in 2014 a $stable check failed in the cycle reset was released, because the previous value was X (Verification Academy, 2014). In 2017 another check failed at start-up because counters were X (Verification Academy, 2017).
  • Why: $stable and $past look at the snapshot before, and that snapshot may still hold reset junk.
  • Fix: guard with disable iff (rst) and start the check from a real event, not from “every cycle”. In 2017 Ben Cohen suggested a ready signal that goes high a few cycles after reset.

FAQ

What does $stable do in SystemVerilog assertions?

It returns true when a signal has the same value as at the previous clock edge.

What is the difference between $past and $stable?

$past(x, N) gives you the old value itself. $stable(x) just tells you whether it changed since the last edge.

What does disable iff do?

While its condition is true, any check in progress is dropped: no pass, no fail.

What is the difference between [*], [->] and [=]?

[*N]: N in a row. [->N]: N times with gaps, ending on the N-th. [=N]: N times with gaps, idle cycles allowed after the N-th.

Why does my assertion fail on the first clock?

Usually $stable or $past comparing against a start-up value. Add a trigger with |-> or |=>, or disable iff (rst).

References

  • IEEE 1800-2017, SystemVerilog standard: section 16.9.2 (repetition in sequences), 16.9.3 (sampled value functions: $past, $stable, $rose), 16.12 (properties, including disable iff and implication).
  • Verification Academy forum threads linked in the mistakes section (accessed 11 Nov 2026).
  • Examples 1 to 3 tested on Verilator 5.020 (--assert), examples 4 and 5 on Synopsys VCS 2025.06, on 11 Nov 2026.

What to read next