Part 4 · Assertions (SVA) · Intermediate

first_match() & Sequence Endpoints

Thread explosion from multiple matches, first_match pruning, and .triggered/.matched endpoint detection.

The multiple-match problem

Every ranged operator multiplies match threads: ##[1:8] forks 8, [*2:5] forks 4, and nesting multiplies them. Multiple matches are not an error — but they bite in two places. First, performance : each live thread is simulator state, and a level antecedent retriggering into a wide range can hold thousands of threads alive. Second, semantics : when a sequence's endpoint feeds something else (a consequent suffix, a .triggered test, a not), every match thread propagates, and the property must hold for the threads you did not intend.

diagram
THREAD EXPLOSION — req ##[1:4] ack ##[1:4] done (one attempt)

attempt tick T
   ├─ ack@T+1 ─┬─ done@T+2
   │           ├─ done@T+3
   │           ├─ done@T+4
   │           └─ done@T+5
   ├─ ack@T+2 ─┬─ done@T+3 ... (4 threads)
   ├─ ack@T+3 ─┬─ ...          (4 threads)
   └─ ack@T+4 ─┬─ ...          (4 threads)
                         = 16 live threads PER ATTEMPT
A level antecedent attempting every cycle for 100 cycles
 up to 1600 concurrent threads from one assertion.

first_match: pruning to the earliest completion

first_match(s) matches only the earliest-ending match of s from a given attempt; the moment any thread of s completes, all other threads of that attempt are killed. If several threads end on the same earliest tick, those ties all survive — first_match prunes by end time, not to a single thread. Use it when you mean "the first time this pattern completes" and when a multi-match sequence's endpoint must be unique for downstream logic.

systemverilog
// Without first_match: every ack in the window spawns a thread
sequence s_any_ack;   req ##[1:8] ack;              endsequence
// With first_match: only the EARLIEST ack ends the sequence
sequence s_first_ack; first_match(req ##[1:8] ack); endsequence

// Endpoint-sensitive use: 'done' must follow the FIRST ack
property p_done_after_first_ack;
  @(posedge clk) $rose(req) |->
    first_match(req ##[1:8] ack) ##1 done;
endproperty
a_fa: assert property (p_done_after_first_ack);
// Without first_match, done after ANY ack-thread endpoint passes —
// a later spurious ack could legitimize a late done.
diagram
WAVEFORM — first_match(req ##[1:8] ack)  (attempt @2)

clk  :  1    2    3    4    5    6    7    8
req  :  0    1    0    0    0    0    0    0
ack  :  0    0    0    1    0    1    0    0
             A         M1        m2
plain sequence : matches at tick 4 AND tick 6 (two endpoints)
first_match    : tick 4 thread completes first  MATCH @4,
                 all sibling threads killed  m2 never reported

Effect on '##1 done' suffix with done only at tick 7:
  plain:        endpoint @6 exists  done@7  PASS (maybe unintended)
  first_match:  endpoint @4 only    done@5? no  FAIL (strict)

Sequence endpoints: .triggered and .matched

A named sequence exposes its endpoint as a boolean: s.triggered is true in any time step where some evaluation of s reaches its endpoint in that step (sampled, usable in expressions, including disable iff conditions and other sequences). s.matched serves the same endpoint-detection purpose for multi-clocked contexts: it persists the endpoint across to the next tick of a destination clock, making it the clock-domain-crossing flavor. Endpoint methods let one sequence act as an event for another property without re-describing the pattern.

systemverilog
sequence s_wr_burst;            // endpoint = last beat of a write burst
  wr_start ##1 wr_beat [->4];
endsequence

// Use the endpoint inside another property (same clock): .triggered
property p_resp_after_burst;
  @(posedge clk) s_wr_burst.triggered |-> ##2 wr_resp;
endproperty
a_resp: assert property (p_resp_after_burst);

// Cross-clock endpoint hand-off: .matched
sequence s_src;   @(posedge clk_a) val_a ##1 done_a;          endsequence
sequence s_pair;  @(posedge clk_b) s_src.matched ##1 done_b;  endsequence
a_cdc: assert property (@(posedge clk_b) start_b |-> s_pair);
diagram
WAVEFORM — s_wr_burst.triggered as an event

clk      :  1    2    3    4    5    6    7    8
wr_start :  0    1    0    0    0    0    0    0
wr_beat  :  0    0    1    1    0    1    0    0
                 A    b1   b2        b4*
.triggered: 0    0    0    0    0    1    0    0
                                     E
E = endpoint tick: the [->4] completes at tick 6  s.triggered
    is 1 exactly there, 0 everywhere else.
The downstream property treats E as a one-tick event:
  s.triggered |-> ##2 wr_resp    wr_resp required @8.

Interview angle and review

Interview angle

  • "What does first_match actually kill?" — sibling threads of the SAME attempt, at the first completed end tick; ties at that tick all survive. "It picks one random match" is the wrong answer.

  • "When is first_match required, not just an optimization?" — when the sequence endpoint feeds downstream timing (suffix elements, .triggered consumers) and only the earliest completion is meaningful.

  • "triggered vs matched?" — both detect endpoints; .triggered is the general same-timestep form, .matched persists across clocks for multi-clock sequence pairing.

  • "Estimate the threads of req ##[1:4] ack ##[1:4] done." — 16 per attempt; multiply the ranges. Interviewers use this to test whether you think in threads at all.

Key takeaways

  • Ranged operators multiply match threads; nested ranges multiply multiplicatively — count them.

  • first_match prunes an attempt to its earliest-ending match(es) and kills the sibling threads.

  • first_match changes meaning, not just speed, whenever the sequence endpoint drives downstream timing.

  • s.triggered turns a sequence endpoint into a boolean event; s.matched is the multi-clock variant.

Common pitfalls

  • Wrapping first_match around a consequent just to silence multiple-match warnings — verify the earliest endpoint is really the spec's intent.

  • Believing first_match always yields exactly one thread — same-tick ties all survive the pruning.

  • Using .triggered on a sequence with ranged operators and assuming a unique fire tick — it pulses at EVERY endpoint unless first_match'd.

  • Using .triggered across clock domains where .matched is required — endpoint sampled in the wrong domain's time step.

  • Ignoring thread cost of level-retriggered wide ranges — assertion-dominated simulation slowdowns that profile as "mysterious".