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.

systemverilog
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
end

break, 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.

systemverilog
// 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");
end

forever + 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.

systemverilog
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
endtask
diagram
KILLABLE 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_n

Key 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.