Part 1 · Language Foundations · Intermediate
Loops & Flow Control
foreach/repeat/while/forever, break/continue, named blocks, disable, and the forever+fork idioms behind monitors.
The loop family and when each fits
Beyond the classic for and while, SystemVerilog adds loops tuned to hardware-verification shapes. foreach iterates an array's declared index space without off-by-one risk, works on every array family (including sparse associative arrays, visiting only existing keys in sorted order), and handles multidimensional arrays with one construct: foreach (m[i][j]). repeat (n) runs a body a fixed count with no loop variable — ideal for “wait 10 clocks”. forever loops until the enclosing process dies; it is the backbone of monitors and clock generators, and it must contain a blocking timing control (@ or #) or it starves the scheduler in a zero-time infinite loop and the simulation hangs.
byte mem [int]; // sparse associative
logic [7:0] frame [4][16];
initial begin
foreach (frame[i][j]) // both dims, bounds from the type
frame[i][j] = i * 16 + j;
foreach (mem[addr]) // only EXISTING keys, ascending
$display("mem[%0d]=%0h", addr, mem[addr]);
repeat (10) @(posedge clk); // wait exactly 10 clock edges
forever begin
@(posedge clk); // REQUIRED: blocks each iteration
if (vif.valid) process_beat();
end
endbreak, continue, named blocks, and disable
break exits the innermost loop and continue jumps to its next iteration — modern, structured, and preferred for ordinary early exits. The older, more powerful mechanism is naming a block (begin : scan_loop) and using disable scan_loop to terminate it — from inside (a multi-level break that break can't do) or even from a different process , which is how watchdogs kill stuck routines. Two cautions: disable of a task name kills every concurrent invocation of that static task scope (another reason for automatic), and abruptly disabling a block that was mid-handshake leaves signals wherever they were — your cleanup code never ran.
// break / continue: structured early exit
foreach (pkt_q[i]) begin
if (pkt_q[i] == null) continue; // skip holes
if (pkt_q[i].is_eof) break; // stop at end-of-frame
process(pkt_q[i]);
end
// Named block + disable: break out of NESTED loops at once
initial begin : search
foreach (table_2d[r]) begin
foreach (table_2d[r][c]) begin
if (table_2d[r][c] == target) begin
$display("hit at [%0d][%0d]", r, c);
disable search; // exits BOTH loops
end
end
end
end
// Cross-process disable: a watchdog killing a stuck wait
initial begin : wait_for_done
@(posedge done);
$display("done seen");
end
initial begin
#10_000;
disable wait_for_done; // give up after timeout
$display("watchdog: done never arrived");
endforever + fork: the monitor idioms
A monitor is, structurally, a forever loop that collects one transaction per iteration — usually one such loop per concern (commands, responses, resets), run in parallel with fork/join_none so the spawning code continues. The companion idiom handles reset: wrap the collection loops in an outer forever that forks them, waits for reset assertion with join_any racing a reset-watcher thread, then disable fork kills the in-flight collectors so they restart clean after reset. Interviewers ask for exactly this “killable monitor” structure; the subtle hazard they probe is that disable fork kills all child processes of the current process — wrap the fork in an isolating fork begin ... end join if other children must survive.
task automatic run_monitor();
forever begin
@(negedge rst_n or posedge rst_n); // align to reset state
wait (rst_n); // idle through reset
fork : collectors
forever collect_cmd(); // thread 1: command channel
forever collect_rsp(); // thread 2: response channel
@(negedge rst_n); // thread 3: reset watcher
join_any // returns when reset asserts
disable fork; // kill in-flight collectors
flush_partial_state(); // drop half-collected txns
end
endtask
// Isolation wrapper when the caller has OTHER children running:
task automatic safe_collect();
fork begin // isolate: disable fork only kills
fork // descendants of THIS begin/end
forever collect_cmd();
@(negedge rst_n);
join_any
disable fork;
end join
endtaskKILLABLE MONITOR — forever + fork/join_any + disable fork
outer forever ──┐
▼
wait (rst_n) ◄───────────────── restart after reset
│
▼
fork ┌──────────────────────────────┐
│ T1: forever collect_cmd() │ runs until killed
│ T2: forever collect_rsp() │ runs until killed
│ T3: @(negedge rst_n) │ completes on reset
└──────────────┬───────────────┘
join_any ◄──────────┘ (T3 finished)
│
▼
disable fork ──► T1, T2 terminated mid-transaction
│
▼
flush_partial_state() ──► loop back, wait for rst_nKey takeaways
foreach iterates the declared index space — bounds-safe, multidimensional, sparse-aware.
forever loops must block on @ or # each pass, or the scheduler starves and simulation hangs.
Named blocks + disable give multi-level and cross-process termination that break cannot.
forever + fork/join_any + disable fork is the standard reset-killable monitor — know the isolation wrapper too.
Common pitfalls
forever (or while) with no timing control in the body — zero-time infinite loop, simulator appears frozen.
disable on a static task name — kills every concurrent invocation, not just yours.
disable fork without an isolation wrapper — silently kills unrelated sibling threads spawned earlier.
Killing collectors with disable and forgetting to flush partial transactions — corrupt data after every reset.