a [*3] means a holds for three consecutive ticks. It is shorthand for a ##1 a ##1 a, and the repeated item can be a whole sequence: (a ##1 b)[*2] expands to a ##1 b ##1 a ##1 b.
// snippet
sequence s_busy4;
start ##1 busy [*4] ##1 done;
endsequence
// Expansion: start ##1 busy ##1 busy ##1 busy ##1 busy ##1 done
// 0 to 2 wait states allowed: ready may come immediately
sequence s_opt_waits;
sel ##1 wait_n [*0:2] ##1 ready;
endsequenceForm | Meaning | Note |
|---|---|---|
|
| Works on whole sequences |
| A run of M to N ticks | One thread per length |
| Empty match, zero ticks | Only glued between neighbors |
| Optional run, possibly absent | The honest way to say maybe |
| Unbounded run | Weak, like |
start ##1 busy [*4] ##1 done spans how many ticks from start to done?Six ticks inclusive: start, four busy ticks, then done. So done lands 5 ticks after start.
assert property (wait_n [*0]) illegal?An empty match consumes zero ticks, so there is no tick at which anything is observed.
Goto [->N] and non-consecutive [=N] both count N scattered occurrences of a boolean. Gaps between occurrences are free in both forms.
They differ in exactly one thing. Goto must end on the tick of the Nth occurrence, while non-consecutive may extend past it through an occurrence-free gap.
// snippet
// LRM expansions: b [->N] === (!b [*0:$] ##1 b) [*N]
// b [=N] === b [->N] ##1 !b [*0:$]The sequences below are not run in this course's checks. Verilator 5.048 stopped with an internal error on [=N], with or without a default clocking block, and it rejects a named sequence with ## after its first item.
// not-run: Verilator 5.048 stops with an internal error on [=N] and rejects these named sequences
sequence s_third_ack_goto;
start ##1 ack [->3]; // ends exactly on the 3rd ack
endsequence
sequence s_third_ack_eq;
start ##1 ack [=3]; // 3 acks total, trailing gap allowed
endsequence
|
| |
|---|---|---|
Counts | N scattered occurrences | N scattered occurrences |
Endpoint | Locked to the Nth occurrence | May drift past it through |
Killed by | Nothing after the Nth tick matters | A further occurrence during the gap |
Default choice | Yes, for protocol timing | Only when the gap is specified |
ack fires during the trailing gap of an [=3] match. What happens to the extended threads?They die. The gap must be occurrence-free, not merely idle time.
One stimulus shows the whole difference. Three ack ticks arrive scattered, and done arrives two ticks after the third one.
The goto endpoint is locked to slot 5, so done at slot 7 misses it. The non-consecutive endpoint drifts to slot 6, so the same done matches.
Slot 0: idle, start, ack, and done all 0.
Slot 1: start rises, and the count begins.
Slot 2: first ack.
Slot 3: second ack.
Slot 4: quiet tick, and both forms tolerate it.
Slot 5: third ack, where the goto endpoint locks.
Slot 6: quiet tick, where only the non-consecutive endpoint may sit.
Slot 7: done rises, one tick past the goto deadline and exactly after the drifted endpoint.
Endpoint alignment is also a placement rule. Time a follow-on off the Nth event with goto, and reach for [=N] only when the follow-on is genuinely decoupled from that cycle.
beat_last. Which operator and join express that?beat_valid [->4] ##0 beat_last. Goto pins the endpoint to the fourth beat, and fusion tests the flag on that very tick.
The bench runs each rule twice as a real assertion. The burst rule is written compact, busy [*4] ##1 done, and written out tick by tick. The counted rule is written with goto, ack [->3] ##1 fin, and with a two-bit counter as modeling code plus a one-step property. Planted bugs: bursts one tick short, bursts where busy drops on the second tick, and a late fin.
10 each. On Verilator 5.048 the compact form reports 5: it catches the short bursts and misses every burst whose busy dropped on the second tick. The written-out form catches all 10.
module tb_repetition;
logic clk = 0, start = 0, busy = 0, done = 0; // burst signals
logic go = 0, ack = 0, fin = 0; // counted-event signals
always #5 clk = ~clk;
int compact_fails = 0, spelled_fails = 0, goto_fails = 0, counted_fails = 0;
// The burst rule twice: compact, and written out tick by tick.
ap_compact: assert property (@(posedge clk) $rose(start) |=> busy [*4] ##1 done) else compact_fails++;
ap_spelled: assert property (@(posedge clk) $rose(start) |=> busy ##1 busy ##1 busy ##1 busy ##1 done) else spelled_fails++;
// The counted rule twice: fin the tick after the third ack.
ap_goto: assert property (@(posedge clk) $rose(go) |=> ack [->3] ##1 fin) else goto_fails++;
// Modeling code counts the acks; a one-step property checks fin.
logic [1:0] n_ack = 0;
always @(posedge clk)
if ($rose(go)) n_ack <= 0;
else if (ack && n_ack != 3) n_ack <= n_ack + 1;
wire third_ack = ack && n_ack == 2;
ap_counted: assert property (@(posedge clk) third_ack |=> fin) else counted_fails++;
task automatic burst(int len, int drop_at = -1);
@(negedge clk) start = 1;
@(negedge clk) start = 0;
for (int i = 0; i < len; i++) begin busy = (i != drop_at); @(negedge clk); end
busy = 0; done = 1;
@(negedge clk) done = 0;
repeat (4) @(negedge clk);
endtask
task automatic three_acks(int g1, int g2, int g3, int late);
@(negedge clk) go = 1;
@(negedge clk) go = 0;
repeat (g1) @(negedge clk); ack = 1; @(negedge clk) ack = 0;
repeat (g2) @(negedge clk); ack = 1; @(negedge clk) ack = 0;
repeat (g3) @(negedge clk); ack = 1; @(negedge clk) ack = 0;
repeat (late) @(negedge clk);
fin = 1; @(negedge clk) fin = 0;
repeat (4) @(negedge clk);
endtask
initial begin
repeat (2) @(negedge clk);
for (int i = 0; i < 10; i++) burst(4); // clean
for (int i = 0; i < 5; i++) burst(3); // planted: one busy tick short
for (int i = 0; i < 5; i++) burst(4, 1); // planted: busy drops on the 2nd tick
$display("bursts (10 clean, 10 bad): compact busy [*4] %0d failures, written out %0d", compact_fails, spelled_fails);
for (int i = 0; i < 10; i++) three_acks(0, 2, 3, 0); // fin right after the third ack
for (int i = 0; i < 5; i++) three_acks(1, 1, 1, 1); // planted: fin one tick late
$display("acks (10 on time, 5 late): ack [->3] ##1 fin %0d failures, counter version %0d", goto_fails, counted_fails);
if (spelled_fails != 10 || counted_fails != 5) $fatal(1, "FAIL: spelled %0d counted %0d", spelled_fails, counted_fails);
$display("PASS: the written-out run and the counter caught every planted bug; compare the compact forms' counts above");
$finish;
end
endmodulebursts (10 clean, 10 bad): compact busy [*4] 5 failures, written out 10
acks (10 on time, 5 late): ack [->3] ##1 fin 0 failures, counter version 5
PASS: the written-out run and the counter caught every planted bug; compare the compact forms' counts aboveRead the output as two lessons. The standard forms are right, and on a commercial simulator they catch every planted bug. On this open simulator the compact forms miss some, and the written-out run and the counter do not. When a check must be trusted on the tool at hand, write it in a form whose verdicts you have tested.
Run with Verilator 5.048 (--binary --timing --assert, plus --coverage when a listing has a cover property) on 2026-10-18; every bench printed the PASS line shown. Verilator stops at the first $error or failing assertion without an action block, so the benches count failures in the action block. Icarus 13 has no concurrent assertions.
busy [*4] in a consequent caught 5 of 10 bad bursts (it missed every drop on the second tick); the written-out form caught 10. ack [->3] ##1 fin caught 0 of 5 late fins; a counter plus fin check caught 5. [=N] stopped Verilator with an internal error.