SystemVerilog · Module 10
randsequence Statement
Compositional grammar generator that mirrors a protocol's BNF; weighted alternatives, repeat-bounded iteration, expansion-time vs execution-time semantics; coexistence with UVM sequences.
Module 10 · Page 10.14
What randsequence Does
randsequence is a procedural statement that generates a random sequence of operations using a context-free grammar. You define named productions — rules that expand into sequences of other productions or direct statements. On each execution the grammar expands from a start production, making random choices at each alternative, until it reaches terminal statements (actual code to execute).
While randcase (page 10.13) picks one branch per execution, randsequence can produce arbitrary-length sequences with structured, grammar-governed ordering — more powerful for modelling protocol flows.
initial begin
randsequence (bus_op) // start production: bus_op
// bus_op expands to one of three alternatives (equal weight)
bus_op : read_txn | write_txn | idle_txn ;
// read_txn: send a read, then optionally a burst follow-up
read_txn : { send_read(); } read_follow ;
// write_txn: always just a single write
write_txn : { send_write(); } ;
// idle_txn: one or more idle cycles
idle_txn : { send_idle(); } ;
// read_follow: either nothing, or another read (recursive bursting)
read_follow : rand join
7 := { } // 70%: stop here
| 3 := { send_read(); } ; // 30%: one more read
endsequence
endSyntax — Productions and Alternatives
The randsequence block contains one or more production rules. Each production has a name, a colon, and a list of alternatives separated by |. The first production listed is the start production, or you can name it explicitly in the opening parentheses.
randsequence (start_prod) // ← named start production (optional)
// ── Production rule ────────────────────────────────────────────
// name : alternative1 | alternative2 | ... ;
// ── Alternative: sequence of items separated by spaces ─────────
// Items can be:
// { statements } — code to execute (terminal)
// production_name — reference to another production (non-terminal)
// if (cond) alt — conditional expansion
// repeat (expr) item — repeat an item N times
start_prod : op_a op_b ; // sequence: always op_a then op_b
op_a : { task_x(); } // terminal: executes task_x
| op_c op_d ; // or: expands op_c then op_d
op_b : { task_y(); } ; // terminal only
op_c : { task_z(); } ;
op_d : { task_w(); } ;
endsequenceWeighted Alternatives — := Operator
Add a weight := alternative prefix to give some alternatives higher probability. The weights follow the same relative-probability rule as dist and randcase.
randsequence (txn)
txn : 6 := { send_read(); } // 60%: read
| 3 := { send_write(); } // 30%: write
| 1 := { send_idle(); } // 10%: idle
;
endsequence
// For sequences (not just single alternatives), weight the whole sequence:
// txn : 7 := read_txn write_txn // 70%: read followed by write
// | 3 := idle_txn // 30%: idle only
// ;repeat and if Inside Productions
Two special constructs inside production alternatives extend the grammar's expressive power without requiring recursive productions.
repeat — execute an item N times
randsequence (burst)
// Generate 2–8 consecutive reads, then a single write
burst : repeat ($urandom_range(2,8)) { send_read(); }
{ send_write(); }
;
endsequenceif — conditional expansion
int error_mode = 0; // set by the test before running
randsequence (op)
op : { setup(); }
if (error_mode) { inject_error(); }
else { normal_op(); }
{ teardown(); }
;
endsequence
// Sequence is always: setup → (inject_error OR normal_op) → teardownbreak — Early Termination
The break statement inside a randsequence block terminates the entire sequence immediately — no further productions are expanded. It is useful for cutting off recursion once a termination condition is met.
int txn_count = 0;
int max_txn = 20;
randsequence (stream)
// Keep generating transactions until the limit is reached
stream : { send_txn();
txn_count++;
if (txn_count >= max_txn) break; }
stream // recurse: generate another
| { } // or stop randomly (base case)
;
endsequencePractical Pattern — AXI Burst Sequence Generator
A realistic protocol sequence: setup a handshake, generate 1–4 data beats, optionally inject a stall, then close with an acknowledge. The grammar enforces ordering while randomising content and count.
randsequence (axi_burst)
// Top level: address phase → data phase → response phase
axi_burst : { send_addr(); }
data_phase
{ recv_response(); }
;
// data_phase: 1 to 4 beats, possibly stalled
data_phase : data_beat // exactly 1 beat
| data_beat stall data_beat // 2 beats with a stall
| data_beat data_beat data_beat // 3 beats
| repeat (4) data_beat // 4 beats, no stall
;
// data_beat: weighted — most are normal, some trigger errors
data_beat : 9 := { send_data(); } // 90%: normal beat
| 1 := { send_data_err(); } // 10%: error beat
;
// stall: 1–3 idle cycles
stall : repeat ($urandom_range(1,3)) { send_idle(); } ;
endsequencerandsequence vs randcase — Choosing the Right Tool
| Scenario | Use |
|---|---|
| Pick one operation from a weighted set per iteration | randcase |
| Generate a structured sequence with ordering rules | randsequence |
| Model a protocol with setup → data → teardown flow | randsequence |
| Drive proportional traffic (60% reads, 40% writes) | randcase or dist |
| Generate recursive or variable-length sequences | randsequence |
| Quick one-liner weighted branch | randcase |
Common Pitfalls
Infinite recursion without a base case
A production that only refers to itself with no non-recursive alternative will loop forever. Always include a base case (an alternative with only terminal statements or an empty body { }) or use break to cut off recursion.
Confusing randsequence with constraints
randsequence has no interaction with the constraint solver or rand variables. It is purely procedural — it selects and executes code, it does not assign values to rand fields.
Using := when | (equal weight) is sufficient
When you just want equal-probability alternatives, plain | is cleaner. Only add := weights when the distribution is intentionally non-uniform.
Forgetting that sequence items execute left-to-right
In a production like A B C, the items execute strictly in order: A fully expands, then B, then C. There is no parallelism. If A is recursive, B and C do not start until A is complete.
Quick Reference
randsequence (start)
// ── Unweighted alternatives (equal probability) ──────────────
start : alt_a | alt_b | alt_c ;
// ── Weighted alternatives ─────────────────────────────────────
start : 6 := alt_a | 3 := alt_b | 1 := alt_c ;
// ── Sequence (items execute left-to-right) ────────────────────
start : item_x item_y item_z ;
// ── Terminal (code to execute) ────────────────────────────────
item_x : { my_task(); } ;
// ── Conditional expansion ─────────────────────────────────────
item_y : if (cond) { task_a(); } else { task_b(); } ;
// ── repeat ────────────────────────────────────────────────────
item_z : repeat ($urandom_range(1,4)) { task_c(); } ;
// ── break — stops the whole randsequence immediately ─────────
item_z : { if (done) break; task_c(); } item_z // recursion
| { } // base case
;
endsequence
// ── Key facts ─────────────────────────────────────────────────
// ✓ Procedural — runs in tasks/functions/initial/always
// ✓ Grammar expands from start production recursively
// ✓ | separates alternatives (equal weight without :=)
// ✓ := assigns weight to an alternative
// ✓ Items in a sequence execute strictly left-to-right
// ✗ No interaction with constraint solver or rand fieldsVerification Usage — Where randsequence Fits in Real Testbenches
randsequence shows up in three reliable architectural roles. Each addresses a need that simpler weighted choice (randcase, dist) cannot express cleanly: structured stimulus with compositional grammar.
Protocol scenario grammars
The canonical use: a protocol allows transaction patterns like "address phase → 1-N data beats → response → optional idle." The grammar mirrors the spec's allowed phrase structure, and randsequence generates legal sequences by walking the grammar.
<code>task axi_seq::body();
randsequence (transaction)
transaction : addr_phase data_beats resp_phase;
addr_phase : { drive_addr(); };
data_beats : repeat($urandom_range(1, 16)) one_beat;
one_beat : { drive_beat(); };
resp_phase : 8 := { drive_resp(); }
| 2 := { drive_error_resp(); };
endsequence
endtask</code>Error-injection scenario libraries
Different error scenarios share most of the protocol grammar but diverge at specific branches — "happy path" picks normal responses; "error injection" picks corrupted responses. randsequence with weighted alternatives encodes both within one grammar.
<code>randsequence (xact)
xact : header payload trailer;
payload : normal_payload | corrupted_payload;
// Weights adjustable per scenario
normal_payload : 95 := { drive_normal_payload(); };
corrupted_payload : 5 := { drive_bad_crc_payload(); };
endsequence</code>Test-layer grammar overrides via embedded code blocks
Embedded code blocks let test-layer configuration influence grammar choices — weights, repeat counts, conditional branches.
<code>randsequence (test_seq)
test_seq : setup body_pattern teardown;
body_pattern : repeat (cfg.burst_count) one_xact;
one_xact : cfg.write_weight := drive_write()
| cfg.read_weight := drive_read()
| 1 := drive_idle(); // safety
endsequence</code>Simulation Behavior — How the Simulator Walks the Grammar
Grammar expansion at runtime
When randsequence (start_symbol) begins, the simulator walks the grammar starting from the named symbol. At each alternative, it evaluates the weight expressions and picks one branch via weighted random choice. The chosen branch may itself be a sequence of sub-productions, which are expanded recursively. The result is a derivation tree of leaves (code blocks) in the order they should execute.
Execution order
After expansion, the simulator visits each leaf code block in tree-traversal order (left-to-right, depth-first) and executes it. Code blocks are arbitrary procedural code — function/task calls, assignments, control flow. They execute synchronously in the calling process.
repeat as bounded iteration
The repeat (N) production form expands the production exactly N times at grammar-expansion time. N is evaluated once when the repeat node is reached; subsequent code-block executions don't re-evaluate it. This gives the simulator bounded knowledge of grammar size — critical for avoiding stack overflow from unbounded recursion.
if-else within productions
The if (cond) production1 else production2 form is evaluated at grammar-expansion time. cond is read once when the if node is reached; based on its value, only one production is added to the derivation tree. The other is never expanded or executed.
No constraint solver involvement
randsequence is procedural — like randcase, it doesn't go through the constraint solver. Weighted choices use the simulator's main PRNG. This makes it fast and class-context-free.
<code>randsequence (s)
s : repeat ($urandom_range(1, 8)) item;
item : { drive_one(); };
endsequence
// Always produces 1-8 items; bounded.</code><code>randsequence (s)
s : 1 := s item
| 1 := item;
item : { drive_one(); };
endsequence
// Recursive expansion: simulator may
// stack-overflow before reaching the
// base case. Use repeat instead.</code>Waveform Analysis — Tracing Generated Sequences
randsequence produces a stream of code-block executions. Logging from each code block makes the generated sequence visible.
The instrumentation pattern
<code>task body();
randsequence (xact)
xact : addr_phase data_phase resp_phase;
addr_phase : { `uvm_info("SEQ", "addr_phase", UVM_HIGH) drive_addr(); };
data_phase : repeat ($urandom_range(1, 4)) one_beat;
one_beat : { `uvm_info("SEQ", "one_beat", UVM_HIGH) drive_beat(); };
resp_phase : 8 := { `uvm_info("SEQ", "ok_resp", UVM_HIGH) drive_resp(); }
| 2 := { `uvm_info("SEQ", "err_resp", UVM_HIGH) drive_err(); };
endsequence
endtask</code>ASCII view — generated sequences on the wire
<code> 100ns SEQ addr_phase
150ns SEQ one_beat
200ns SEQ one_beat
250ns SEQ one_beat
300ns SEQ ok_resp ← happy path (80% probability)
...
900ns SEQ addr_phase
950ns SEQ one_beat
1000ns SEQ one_beat
1050ns SEQ err_resp ← error path (20% probability)
↑ ↑
transaction structured pattern visible:
boundary addr → 1-4 beats → resp</code>Coverage validation per production
Pair each production with a coverpoint that bins by which alternative was chosen. The coverage proves each alternative actually fires; bins firing at unexpected frequencies surface weight or grammar bugs early.
Industry Insights — Hard-Won Lessons About randsequence
Debugging Academy — Five Real Bugs From randsequence Misuse
"Left-recursive production crashes the simulator"
DEBUGA test runs the first transaction successfully then the simulator dies with stack overflow. The trace points at randsequence grammar expansion.
<code>randsequence (list)
list : 1 := list item // BUG: left recursion
| 1 := item;
item : { drive_one(); };
endsequence</code>The simulator expands list by either taking the recursive branch (which calls list again) or the base branch (terminating). With equal weights, it may recurse 1000 times before terminating — eventually exhausting the stack.
Use repeat for bounded iteration: randsequence (list) list : repeat ($urandom_range(1, 16)) item; item : { drive_one(); }; endsequence. The simulator knows up-front how many iterations to expand; no recursion, no stack risk.
"Weighted alternative fires far less than expected"
DEBUGA randsequence declares 80 := happy | 20 := error but coverage shows happy:error ratio of 99:1.
<code>randsequence (xact)
xact : happy | error;
happy : 80 := { drive_normal(); }; // expected 80%
error : 20 := { drive_error(); }; // expected 20%
// actual: ~99% happy
endsequence
// Issue: weights belong on the alternatives in `xact`, not on the productions</code>Weights are written on the wrong side. In xact : happy | error;, the alternatives between happy and error are equally weighted (no weight = weight 1). The "80" and "20" on production bodies are not weights — they may be silently ignored or interpreted as something else depending on the simulator.
Put weights on the alternatives in the parent production: randsequence (xact) xact : 80 := happy | 20 := error; happy : { drive_normal(); }; error : { drive_error(); }; endsequence. Now the 80/20 weighting applies as intended.
"Code block updates variable, later branch reads stale value"
DEBUGA grammar where one code block sets a flag and a later branch checks the flag appears to never see the updated value.
<code>int err_count = 0;
randsequence (s)
s : set_err check_err;
set_err : { err_count = 5; };
check_err : if (err_count > 0)
{ drive_error_handler(); }
else
{ drive_normal(); };
endsequence
// drive_normal() runs even though set_err just set err_count = 5</code>The if in the grammar is evaluated at expansion time, before any code blocks run. err_count is still 0 at expansion; the grammar tree commits to drive_normal() before set_err ever executes.
Move the conditional logic into the code block: check_err : { if (err_count > 0) drive_error_handler(); else drive_normal(); };. Now the condition is evaluated at execution time, after the earlier code block has run. The if at grammar level is for structural decisions made before any code runs; runtime decisions belong inside code blocks.
"Production name collision with task name causes wrong behaviour"
DEBUGA grammar uses a production named drive_xact which happens to also be the name of an external task. The compiler accepts it but behaviour is unexpected.
<code>task drive_xact();
...
endtask
task body();
randsequence (xact)
xact : repeat (5) drive_xact; // BUG: collides with task name
drive_xact : { drive_one(); }; // production
endsequence
endtask</code>The production name drive_xact shadows the task with the same name in the local scope. Some simulators warn; others silently choose one or the other; behaviour is unpredictable across tools.
Use distinct names for productions vs external tasks. Production naming convention that helps: prefix productions with "p_" or "seq_" so they're visually distinguishable from regular task/function names. seq_drive_xact or p_drive_xact can never collide with a task named drive_xact.
"All weights evaluate to 0 — production fails to select"
DEBUGA grammar with config-driven weights occasionally fails with "no production selected" or silent fall-through.
<code>randsequence (xact)
xact : cfg.write_w := write_seq
| cfg.read_w := read_seq;
// If both cfg fields are 0 → no selection possible
write_seq : { drive_write(); };
read_seq : { drive_read(); };
endsequence</code>Same as the randcase all-zero-weights case: when every alternative's weight evaluates to 0, the simulator cannot select. Behaviour is implementation-defined.
Always include a literal-weight safety alternative: xact : cfg.write_w := write_seq | cfg.read_w := read_seq | 1 := idle_seq;. The safety branch fires when others are all 0; otherwise it's a small uniform contribution. Defensive practice for any grammar with expression weights.
Interview Q&A — Twelve Questions You Will Be Asked
Generates structured random stimulus by walking a grammar of production rules. Each production names a sequence of sub-productions and/or code blocks; weighted alternatives let the simulator pick one of several paths at each branching point. The result is a sequence of code-block executions in tree-traversal order.
Best Practices — Ten Rules for randsequence Discipline
- Use
randsequencefor compositional grammar structure; userandcasefor single weighted choices. Different tools for different complexity levels. - Always prefer
repeat (N) productionover left-recursive productions. Bounded expansion avoids stack overflow risk entirely. - Always include a literal-weight safety alternative when weights are expressions. Prevents the all-zero-weights implementation-defined case.
- Mirror the protocol spec's BNF in your grammar. The verification grammar then reads like the spec grammar — direct review, fast updates.
- Keep productions small and composable. Each production should be a short sequence of sub-productions plus optionally one code block. Factor longer productions into sub-grammars.
- Use distinct names for productions vs external tasks/functions. Name collisions cause implementation-defined behaviour. Prefix productions (
p_,seq_) to make them visually distinct. - Remember grammar-level
ifandrepeatevaluate at expansion time. Runtime decisions must live inside code blocks, not in the grammar structure. - Log production names from code blocks during bring-up. Production names in logs make grammar-path diagnosis a one-line log scan.
- Pair every interesting alternative with a coverage bin. The coverage proves each grammar path is actually exercised.
- Don't synthesise
randsequence. Strictly simulation-only; keep in verification directories or behind ``ifdef` guards.