Skip to content

break, continue, return, disable

Loop control — synthesis vs simulation behavior.

Module 5 · Page 5.5

break — Exit a Loop Early

break immediately exits the innermost loop containing it. Execution resumes at the statement after the end of that loop. It works inside for, while, do-while, repeat, and forever.

In synthesisable RTL, break is valid as long as the enclosing loop can still be statically unrolled — the tool unrolls the loop and inserts logic equivalent to stopping iteration at the right point.

SystemVerilog — break
// ── Find first set bit (priority encoder) ───────────────────────
always_comb begin
first_set = 4'hF;          // default: no bit found
for (int i = 0; i < 8; i++) begin
if (data[i]) begin
first_set = i[3:0];
break;              // stop as soon as first '1' found
end
end
end
 
// ── Testbench: stop listening once error seen ───────────────────
initial begin
for (int pkt = 0; pkt < 1000; pkt++) begin
@(posedge clk);
if (error_flag) begin
$error("Error at packet %0d", pkt);
break;              // no point sending more after an error
end
drive_packet(pkt);
end
end
 
// ── break in nested loops: only exits the innermost loop ────────
initial begin
for (int r = 0; r < 4; r++) begin      // outer loop continues
for (int c = 0; c < 4; c++) begin
if (c == 2) break;            // exits inner loop at c=2
$display("[%0d][%0d]", r, c);  // prints [r][0] and [r][1] only
end
end
end

🏗 Synthesis Insight: What break Produces in Hardware

When synthesis unrolls a for loop containing a break, it does not generate hardware that stops executing — there is no "halt" concept in gates. Instead, it generates a found flag that propagates through the unrolled iterations, gating all subsequent assignments. The hardware is equivalent to: iterate all N positions, but only commit the result of the first match. For a priority encoder with break, synthesis produces a priority tree — identical to writing it with cascaded if-else if. The loop with break is just a more readable way to express the same hardware intent.

🧠 How the Simulator Handles break in always_comb

In simulation, break inside always_comb causes the for loop to terminate at that iteration. Subsequent iterations do not execute. Since the entire always_comb block runs in zero simulation time, this early exit saves simulation CPU cycles — a loop with break is faster to simulate than one that runs all iterations. The output is computed correctly because the default assignment before the loop covers all positions that were not reached.

continue — Skip to the Next Iteration

continue skips the rest of the current loop body and jumps directly to the next iteration check. The loop itself does not exit — only the current iteration is cut short. Think of it as "skip this one, try the next."

SystemVerilog — continue
// ── Sum only even-indexed elements ──────────────────────────────
always_comb begin
even_sum = '0;
for (int i = 0; i < 8; i++) begin
if (i[0]) continue;     // skip odd indices (i[0]=1 when odd)
even_sum += data[i];
end
end
 
// ── Testbench: log only valid transactions ───────────────────────
initial begin
for (int i = 0; i < 100; i++) begin
@(posedge clk);
if (!valid) continue;    // skip idle cycles
$display("[%0t] data=%0h", $time, data);
end
end
 
// ── continue vs break side-by-side ──────────────────────────────
initial begin
for (int i = 0; i < 5; i++) begin
if (i == 3) continue;    // prints 0,1,2,4  (skips 3)
$display("i = %0d", i);
end
 
for (int i = 0; i < 5; i++) begin
if (i == 3) break;       // prints 0,1,2    (stops at 3)
$display("i = %0d", i);
end
end

🚀 RTL Design Insight: continue Is a Readable Way to Express Selective Logic

In synthesis, continue inside a for loop generates an if condition that gates the body of that iteration. if(i[0]) continue; sum += data[i]; is identical to if(!i[0]) sum += data[i]; in hardware. The continue version reads as "skip odd indices" — the intent is clearer. Both produce identical netlists. The choice between them is purely a code-readability decision. Use whichever makes the filtering condition most obvious to the next engineer reading the code.

return — Exit a Function or Task

return exits the current function or task immediately. In a function, you can optionally provide a return value: return expr;. In a task, return takes no value — it just exits the task.

SystemVerilog — return in functions and tasks
// ── return with value in a function ─────────────────────────────
function automatic int clz(input logic [7:0] data);
for (int i = 7; i >= 0; i--)
if (data[i]) return 7 - i;   // count leading zeros from MSB
return 8;                           // all zeros → 8
endfunction
 
// ── return in a task (no value) ─────────────────────────────────
task automatic send_packet(input int size);
if (size <= 0) begin
$warning("send_packet: invalid size %0d, aborting", size);
return;                         // early exit, no further execution
end
for (int b = 0; b < size; b++) begin
@(posedge clk);
data = $random;
valid = 1;
end
valid = 0;
endtask
 
// ── Multiple return paths (guard clauses pattern) ───────────────
function automatic logic [7:0] saturate_add(
input logic [7:0] a, b);
logic [8:0] sum = a + b;
if (sum[8]) return 8'hFF;  // overflow → saturate at max
return sum[7:0];             // normal result
endfunction

💡 Senior Verification Engineer Tip: return as a Guard Clause Pattern

The guard clause pattern — using early return to reject invalid inputs at the top of a function — dramatically improves readability compared to deeply-nested if blocks. Instead of if(valid) { if(size>0) { ... } }, write if(!valid) return; if(size<=0) return; followed by the main logic. This pattern is universally adopted in production UVM driver code: each guard clause handles one error condition, the main logic at the bottom only runs with valid inputs, and each return exit point can have its own $error message for clear debugging.

disable — Kill a Named Block or Task

disable has two forms:

  • disable label; — immediately terminates the labelled block or named task.
  • disable fork; — kills all child processes spawned by the current process (all threads in a fork-join_any or fork-join_none).

disable is primarily a simulation-only construct. It is not synthesisable. Use it in testbenches and verification environments.

SystemVerilog — disable label and disable fork
// ── disable label: exit a named block ──────────────────────────
initial begin
// label a named block with: begin : label_name
begin : search_block
for (int i = 0; i < 16; i++) begin
if (mem[i] == target) begin
found_idx = i;
disable search_block;   // jump out of the named block
end
end
found_idx = -1;               // not reached if disable fires
end
end
 
// ── disable fork: kill spawned threads ──────────────────────────
initial begin
fork
begin : timeout_proc
#10_000;               // watchdog: 10 000 time units
$fatal("TIMEOUT");
end
begin : main_proc
run_test();             // actual test sequence
end
join_any
disable fork;               // kill whichever thread didn't finish
end
 
// ── disable task (kills an executing named task) ────────────────
task automatic long_driver();
for (int i = 0; i < 1000; i++) begin
@(posedge clk);
data = i;
end
endtask
 
initial begin
fork
long_driver();
join_none
#200;
disable long_driver;        // abort task after 200 time units
end

🔍 Debugging Insight: disable fork Is Stateful — It Kills ALL Current Children

disable fork terminates all child threads spawned by the current process that are still running — not just the ones you intended to stop. If you have three forked threads (stimulus driver, coverage sampler, protocol monitor) and one finishes, calling disable fork kills the remaining two simultaneously. This is often intentional at test completion, but it becomes a bug when called too early. The safest pattern: use named process labels (begin : my_thread) and disable my_thread to kill specific threads rather than using the broad disable fork.

Quick Reference

KeywordApplies toEffectSynthesisable?
breakLoops (for, while, do-while, repeat, forever)Exits innermost loop immediatelyYes, if loop unrolls
continueLoopsSkips rest of current iteration, jumps to nextYes, if loop unrolls
returnFunctions and tasksExits function/task; function can return a valueYes
disable label;Named blocks, named tasksTerminates the named scope and all statements inside itNo — simulation only
disable fork;Current process's child threadsKills all threads spawned by the current processNo — simulation only

🏗 Synthesis Behavior — What the Tool Generates

Understanding what synthesis produces from each control keyword prevents surprises during gate-level simulation and timing analysis. The hardware generated is often less obvious than the simulation behavior.

KeywordSynthesizable?ConditionHardware GeneratedSim/Synth Match?
breakYesInside a loop with constant unrollable bound"Found" flag gates remaining iterations — priority logicYes — identical priority behavior
continueYesInside a loop with constant unrollable boundCondition gate on that iteration's body — equivalent to if(!cond)Yes — identical selective evaluation
return (function)YesAlways synthesizable in functions called from always_combMultiplexer selecting which return path's value drives outputYes
return (task)ConditionalOnly if task has no timing controls and loop bounds are staticEarly-exit condition on subsequent logicUsually yes, verify per tool
disable labelNoNever — requires process modelN/A — simulation onlyN/A
disable forkNoNever — requires forked thread modelN/A — simulation onlyN/A
SystemVerilog — break/continue: equivalent hardware without the keyword
// ── break in for loop — synthesis perspective ─────────────────────
always_comb begin
first = 4'hF; // default
for (int i = 0; i < 8; i++) begin
if (data[i]) begin
first = i[3:0];
break;          // stop at first '1'
end
end
end
 
// ── Synthesis unrolls to equivalent hardware (NO break in netlist):
always_comb begin
logic found;
first = 4'hF; found = 0;
if (!found && data[0]) begin first = 0; found = 1; end  // i=0
if (!found && data[1]) begin first = 1; found = 1; end  // i=1
if (!found && data[2]) begin first = 2; found = 1; end  // i=2
// ... and so on for all 8 iterations
// Hardware: cascade of AND gates checking !found — priority encoder
end
 
// ── continue in for loop — synthesis perspective ──────────────────
always_comb begin
even_sum = '0;
for (int i = 0; i < 8; i++) begin
if (i[0]) continue;  // skip odd
even_sum += data[i];
end
end
 
// ── Synthesis unrolls to (hardware identical):
always_comb begin
even_sum = '0;
if (!1'b0) even_sum += data[0];   // i=0: i[0]=0, not skipped
// if (!1'b1) → constant false → data[1] never added (optimized away)
if (!1'b0) even_sum += data[2];   // i=2: i[0]=0, not skipped
// ... pattern continues
// Hardware: adder with 4 inputs (data[0,2,4,6]). No gates for odd indices.
end

🔬 RTL vs Verification — Which Keyword Belongs Where

Each of these keywords has a dominant use context. Knowing which tool to reach for prevents both synthesis errors and verification logic bugs.

KeywordPrimary RTL UsePrimary Verification UseNever Use Here
breakPriority encoder: stop scanning at first match. Find-first-set. Early-exit search in always_comb for loops.Stop test on first error. Exit coverage-driven loop when goal met. Abort stimulus after fault injection.Inside forever in simulation — exits the forever, ending the process unexpectedly
continueSelective accumulation: skip specific indices. Filtered parallel operations (sum only valid bits).Skip idle/invalid cycles in monitor loops. Filter transactions in scoreboard loops.Inside forever without a timing control — causes zero-time loop hang
returnGuard clauses in synthesizable functions. Early return from function on saturate/overflow condition.Guard clauses in driver tasks. Early exit from task on invalid parameter. Multi-path function results.Returning from always_comb block body (return is for functions/tasks only)
disableNot synthesizable — never use in RTL intended for synthesisTimeout watchdog (disable fork after join_any). Killing a concurrent stimulus thread. Aborting a running named task.In synthesizable RTL of any kind

⚙ RTL Patterns — Real Hardware Using break and continue

SystemVerilog — RTL Patterns: break and continue in Real Hardware
// ── Pattern 1: Priority encoder with break ────────────────────────
module prio_enc #(parameter int N = 8) (
input  logic [N-1:0]     req,
output logic [$clog2(N):0] grant_id,
output logic              valid
);
always_comb begin
grant_id = '0;
valid    = 1'b0;
for (int i = 0; i < N; i++) begin
if (req[i]) begin
grant_id = i[$clog2(N):0];
valid    = 1'b1;
break;           // req[0] is highest priority — stop here
end
end
end
endmodule
// break here: synthesis generates a standard priority encoder tree
// Equivalent to priority if chain — break makes the code read naturally
 
// ── Pattern 2: Checksum over valid bytes (continue skips invalid) ─
module valid_checksum (
input  logic [7:0] data [16],
input  logic        valid [16],  // per-byte valid mask
output logic [7:0] checksum
);
always_comb begin
checksum = 8'h00;
for (int i = 0; i < 16; i++) begin
if (!valid[i]) continue;   // skip invalid bytes
checksum ^= data[i];          // XOR only valid bytes
end
end
endmodule
// continue: synthesis generates 16 AND gates (valid[i] & data[i])
// feeding an XOR tree. Invalid bytes produce 0 contribution → correct
 
// ── Pattern 3: Memory search with break (find match in TCAM style) ─
module cam_lookup #(parameter int DEPTH=16, WIDTH=8) (
input  logic [WIDTH-1:0] key,
input  logic [WIDTH-1:0] mem [DEPTH],
output logic [$clog2(DEPTH):0] match_addr,
output logic               hit
);
always_comb begin
match_addr = '0;
hit        = 1'b0;
for (int i = 0; i < DEPTH; i++) begin
if (mem[i] == key) begin
match_addr = i[$clog2(DEPTH):0];
hit        = 1'b1;
break;         // return lowest-address match
end
end
end
endmodule

🛡 Return Guard Clauses — Cleaner, Safer Functions and Tasks

The guard clause pattern uses early return statements to handle edge cases and invalid inputs at the top of a function or task, before the main logic runs. It makes code flatter, more readable, and easier to test. ❌ Deep nesting — hard to readtask automatic send_burst( input int len, input logic [31:0] addr ); if (len > 0) begin if (addr != 0) begin if (!bus_busy) begin // actual work buried here for(int i=0; i<len; i++) begin @(posedge clk); drive_data(i); end end else $error("Bus busy"); end else $error("Bad addr"); end else $error("Bad len"); endtask✅ Guard clauses — flat and cleartask automatic send_burst( input int len, input logic [31:0] addr ); // Guard clauses — reject bad inputs early if (len <= 0) begin $error("Bad len"); return; end if (addr == 0) begin $error("Bad addr");return; end if (bus_busy) begin $error("Busy"); return; end // Main logic — all guards passed for(int i=0; i<len; i++) begin @(posedge clk); drive_data(i); end endtask

SystemVerilog — return in Synthesizable Functions (Multiple Return Paths)
// ── Saturating arithmetic with multiple return paths ──────────────
function automatic logic [7:0] sat_add8(
input logic [7:0] a, b);
logic [8:0] wide = a + b;
if (wide[8]) return 8'hFF;    // overflow → saturate max
return wide[7:0];              // normal
endfunction
// Synthesis: 9-bit adder → 2:1 mux on bit[8] → saturate or pass
 
// ── Priority encode with guard and early return ──────────────────
function automatic logic [2:0] penc3(
input logic [7:0] req);
if (req == 8'h00) return 3'd7;  // no request → sentinel value
for (int i = 0; i < 8; i++)
if (req[i]) return i[2:0];  // first set bit → priority encode
return 3'd7;                       // unreachable but satisfies tool
endfunction
// Guard clause (req==0 check) is synthesized as a separate 8-input NOR
// feeding a 2:1 mux that selects between sentinel and the encoder output
 
// ── AXI response handler with error guard ────────────────────────
function automatic logic [7:0] decode_axi_resp(
input logic [1:0] resp,
input logic        valid);
if (!valid)            return 8'hFF;  // guard: not valid
if (resp == 2'b10)    return 8'hFE;  // SLVERR
if (resp == 2'b11)    return 8'hFD;  // DECERR
return 8'h00;                         // OKAY/EXOKAY
endfunction

⚡ Concurrent Process Control — disable in Real Verification

disable is the verification engineer's tool for managing concurrent simulation threads. Every production testbench uses it — typically in the timeout watchdog and test cleanup patterns.

SystemVerilog — disable fork: Timeout Watchdog and Test Cleanup
// ── Pattern 1: Timeout watchdog using fork-join_any + disable fork
module tb_with_watchdog;
initial begin
fork
begin : test_body
run_directed_test();   // main stimulus
run_random_test();
end
begin : watchdog
#1_000_000;           // timeout: 1M time units
$fatal(1, "WATCHDOG: test timed out");
end
join_any
disable fork;             // kill whichever thread is still running
// Control resumes here after EITHER test_body OR watchdog finishes
$display("Test complete at t=%0t", $time);
$finish;
end
endmodule
 
// ── Pattern 2: Coverage-driven test with disable label ────────────
initial begin
begin : coverage_test
int max_cycles = 100_000;
for (int cycle = 0; cycle < max_cycles; cycle++) begin
@(posedge clk);
drive_random_stimulus();
if (coverage_goal_met()) begin
$display("Coverage goal met at cycle %0d", cycle);
disable coverage_test;   // exit the named block early
end
end
$display("Coverage NOT met after %0d cycles", max_cycles);
end
end
 
// ── Pattern 3: Protocol handshake with configurable timeout ───────
task automatic wait_for_handshake(
input int timeout_cycles,
output bit timed_out
);
timed_out = 0;
fork
begin : hs_wait
@(posedge clk iff (valid && ready));  // wait for handshake
end
begin : hs_timeout
repeat(timeout_cycles) @(posedge clk);
timed_out = 1;
end
join_any
disable fork;   // clean up whichever thread is still waiting
if (timed_out) $error("Handshake timeout after %0d cycles", timeout_cycles);
endtask

fork-join_any + disable fork: which thread finishes first?Time →0 100 200 300 400 500test_body[───────────────────────DONE──────────────────]watchdog[──────────────────────────────────────────────T/O] Scenario A: test_body finishes at T=300 (before watchdog at T=500) → join_any fires at T=300 → disable fork kills watchdog thread → "Test complete" message printed. Normal exit.Time →0 100 200 300 400 500test_body[──────────────────────────────────────────────HUNG] watchdog[───────────────────────────────────────TIMEOUT─]Scenario B: test_body hangs, watchdog fires at T=500 → join_any fires at T=500 → disable fork kills test_body (stuck) thread → $fatal message. Simulation aborts.

🔬 Advanced Verification Patterns

SystemVerilog — Complete Verification Environment Using All Four Keywords
module tb_complete;
logic clk = 0, rst_n;
logic [7:0] data_in, data_out;
logic       valid, ready, error_flag;
int         pass_cnt=0, fail_cnt=0, tx_cnt=0;
 
// ── Clock ─────────────────────────────────────────────────────
initial forever #5 clk = ~clk;
 
// ── Reset ─────────────────────────────────────────────────────
initial begin
rst_n = 0;
repeat(4) @(posedge clk);
rst_n = 1;
end
 
// ── Stimulus driver using break and continue ──────────────────
task automatic drive_stimulus();
@(posedge rst_n);
for (int i = 0; i < 1000; i++) begin
@(posedge clk);
if (error_flag) begin
$error("Error at i=%0d — stopping stimulus", i);
break;               // stop on first error
end
if (!ready) continue;   // skip when DUT not ready
data_in = $urandom;
valid   = 1'b1;
tx_cnt++;
end
valid = 1'b0;
endtask
 
// ── AXI response handler using return guard ───────────────────
task automatic check_output();
@(posedge clk);
if (!valid)   return;        // guard: not a valid transaction
if (error_flag) return;       // guard: DUT in error state
if (data_out === ~data_in) pass_cnt++;
else begin
$error("got %h exp %h", data_out, ~data_in);
fail_cnt++;
end
endtask
 
// ── Main test with timeout watchdog using disable fork ────────
initial begin
fork
begin : test_main
drive_stimulus();
$display("Done: tx=%0d pass=%0d fail=%0d",tx_cnt,pass_cnt,fail_cnt);
end
begin : watchdog
#500_000;
$fatal(1, "WATCHDOG timeout");
end
join_any
disable fork;
$finish;
end
 
// ── Monitor (uses continue to skip non-valid cycles) ─────────
initial forever begin
@(posedge clk);
if (!valid) continue;      // wait for valid — skip idle
check_output();
end
endmodule

🔬 Debugging Academy — 8 Real Bugs from the Field

1break Inside forever — Exits the Loop, Kills the ProcessSilent Process DeathBuggy Code

Bug 1 — break Exits forever Loop, Silently Stopping the Monitor
// ❌ BUG: engineer intended to stop logging on error, but break exits forever
initial begin
forever begin
@(posedge clk);
if (valid) $display("[%0t] data=%h", $time, data_out);
if (error_flag) begin
$error("Error detected");
break;   // ❌ exits the forever loop entirely!
// Monitor STOPS. Error reported at T=X,
// but subsequent transactions NEVER checked.
// Test appears to pass — missing error detections.
end
end
end
 
// ✅ FIX 1: Don't break from monitor — just flag the error and continue
initial begin
forever begin
@(posedge clk);
if (valid) $display("[%0t] data=%h", $time, data_out);
if (error_flag) $error("Error detected");  // continue monitoring
end
end
 
// ✅ FIX 2: If you want to stop the test, use disable or $finish
initial begin
forever begin
@(posedge clk);
if (error_flag) begin $fatal(1, "Fatal error"); end  // ends sim
end
end

1Root Cause / ImpactRoot Causebreak exits the innermost enclosing loop. When the innermost loop is forever, break exits it — terminating the forever permanently. The initial block completes its execution (the forever was all that was left), and the process dies. The monitor is now dead: all subsequent transactions go unchecked.Why It's DangerousThis bug causes the regression to silently undercheck. An error is reported at T=X, but errors at T=X+100 are never detected. The test reports pass/fail counts that only include transactions up to the first error. The coverage also stops accumulating. Results look almost-correct but are incomplete.2continue Inside forever Without Timing — Zero-Time Loop HangZero-Time HangBuggy Code

Bug 2 — continue in forever Without Timing Produces Zero-Time Loop
// ❌ BUG: continue re-enters forever body immediately (no time advance)
initial begin
forever begin
if (!valid) continue;  // ❌ skips to next forever iteration
// but there's no timing before this check!
// valid never changes → zero-time loop
$display("%h", data);
end
end
// Simulation: hangs at time 0. CPU=100%. No output. Same as forgetting @.
 
// ✅ FIX: ALWAYS have a timing control before continue in forever
initial begin
forever begin
@(posedge clk);         // ✅ timing BEFORE the continue check
if (!valid) continue;  // ✅ safe: re-enters AFTER next posedge
$display("%h", data);
end
end

3return From Task Without Clearing Output Signals — DUT StuckSignal Cleanup BugBuggy Code

Bug 3 — Task Returns Without Deassert, Leaving DUT in Bad State
// ❌ BUG: task returns early but leaves valid=1 asserted permanently
task automatic drive_packet(input int size, input logic [7:0] payload[]);
if (size <= 0) return;   // ❌ returns but valid/ready may be asserted!
 
valid = 1'b1;
for (int i = 0; i < size; i++) begin
data = payload[i];
@(posedge clk);
if (bus_error) return;   // ❌ returns mid-burst, valid still=1!
// DUT sees endless burst after task exits
end
valid = 1'b0;  // this is only reached if loop completes normally
endtask
 
// ✅ FIX: always clean up signals before return (or use defer-style cleanup)
task automatic drive_packet(input int size, input logic [7:0] payload[]);
if (size <= 0) return;   // safe — valid not yet asserted
 
valid = 1'b1;
for (int i = 0; i < size; i++) begin
data = payload[i];
@(posedge clk);
if (bus_error) begin
valid = 1'b0;     // ✅ clean up BEFORE return
$error("Bus error at beat %0d", i);
return;
end
end
valid = 1'b0;
endtask

4disable With Wrong Label — Block Not Disabled, Continues RunningLabel TypoBuggy Code

Bug 4 — disable With Wrong Label Name: Block Keeps Running
// ❌ BUG: label name typo — 'seach_blk' instead of 'search_blk'
initial begin
begin : search_blk                  // ← correct label here
for (int i = 0; i < 16; i++) begin
if (mem[i] == target) begin
found_idx = i;
disable seach_blk;   // ❌ TYPO: 'seach' not 'search'
// In some simulators: compile error
// In others: runtime — no effect!
// Loop runs all 16 iterations
// found_idx = last match (not first)
end
end
found_idx = -1;               // this runs even after match!
end
end
 
// ✅ FIX: use break for simple in-loop exit (no label needed)
initial begin
found_idx = -1;
for (int i = 0; i < 16; i++) begin
if (mem[i] == target) begin
found_idx = i;
break;   // ✅ no label required, no typo risk
end
end
end

4Engineering LessonEngineering LessonPrefer break over disable for in-loop exit.break requires no label, cannot have a typo, and is explicit about exiting the loop. Reserve disable label for the specific case where you need to exit a named block from outside a loop (e.g., exiting a sequenced block based on an internal condition). disable fork is the right tool for concurrent process management. Both have their place — but break is always the right choice for "exit this loop early."5disable fork Kills Needed Concurrent ThreadsCollateral Process KillBuggy Code

Bug 5 — disable fork Kills All Children, Including Needed Ones
// ❌ BUG: disable fork kills the coverage sampler along with the stimulus
initial begin
fork
stimulus_driver();     // drives 1000 transactions
coverage_sampler();    // needs to run AFTER stimulus finishes
watchdog();
join_any
disable fork;             // ❌ kills ALL — coverage sampler killed too!
// Coverage never completes — coverage reports are incomplete
end
 
// ✅ FIX: use named labels to kill only specific threads
initial begin
fork
begin : stim_proc   stimulus_driver();  end
begin : cov_proc    coverage_sampler(); end
begin : wdog_proc   watchdog();         end
join_any
disable stim_proc;    // ✅ kill only stimulus
disable wdog_proc;    // ✅ kill only watchdog
// coverage sampler (cov_proc) continues running
end

6break Only Exits Innermost Loop — Outer Loop ContinuesNested Loop ConfusionBuggy Code

Bug 6 — break in Inner Loop Does Not Exit Outer Loop
// ❌ BUG: engineer expected break to exit both loops — it only exits inner
initial begin
for (int r = 0; r < 8; r++) begin    // outer: row 0..7
for (int c = 0; c < 8; c++) begin  // inner: col 0..7
if (mem[r][c] == target) begin
found_r = r; found_c = c;
break;     // ❌ exits INNER loop only!
// Outer loop continues from r=found_r+1
// More searches happen → found_r/found_c overwritten
end
end
end
end
 
// ✅ FIX: use a 'found' flag to gate the outer loop
initial begin
bit found = 0;
found_r = -1; found_c = -1;
for (int r = 0; r < 8 && !found; r++) begin
for (int c = 0; c < 8; c++) begin
if (mem[r][c] == target) begin
found_r = r; found_c = c;
found = 1;
break;         // ✅ exits inner, outer checks !found
end
end
end
end

7Function With No return on One Path — Synthesizer Gets Wrong ValueMissing Return PathBuggy Code

Bug 7 — Function Missing return on One Code Path
// ❌ BUG: function has no return on the else path
function automatic logic [7:0] compute(input logic [7:0] a, b);
if (a > b) begin
return a - b;   // path A: explicit return ✅
end
// path B: no return statement!
// SystemVerilog: function implicitly returns function_name variable
// which was never assigned → returns 0 (or X in 4-state)
// Simulation: returns 0 when a <= b
// This matches "b - a = 0 when a==b" accidentally → test passes
// But when a=3, b=5: returns 0 instead of 5-3=2 — WRONG!
endfunction
 
// ✅ FIX: always have an explicit return on every path
function automatic logic [7:0] compute(input logic [7:0] a, b);
if (a > b) return a - b;   // ✅ path A
return b - a;                  // ✅ path B — abs(a-b)
endfunction
// Lint tools catch "function may have no return on all paths" — treat as ERROR

8continue Where break Was Intended — Loop Runs All Iterations UnnecessarilyWrong Control FlowBuggy Code

Bug 8 — continue Used Instead of break: Finds Last Match, Not First
// ❌ BUG: continue skips remaining body but keeps looping
// Engineer wanted to stop at first match — used continue instead of break
always_comb begin
first_set = 4'hF;
for (int i = 0; i < 8; i++) begin
if (!data[i]) continue;  // skip iterations where data[i]=0
first_set = i[3:0];        // ← BUT this runs for EVERY set bit!
// Result: first_set = LAST set bit (not first)
// RTL synthesis: not a priority encoder — last-wins mux chain
end
end
 
// Example: data=8'b0001_0100 (bits 2 and 4 set)
// With continue: first_set = 4 (last set bit) ❌
// With break:    first_set = 2 (first set bit) ✅
 
// ✅ FIX: use break to stop at first match
always_comb begin
first_set = 4'hF;
for (int i = 0; i < 8; i++) begin
if (data[i]) begin
first_set = i[3:0];
break;   // ✅ stops at first match — priority encoder behavior
end
end
end

8The Key Mental ModelMental Modelcontinue:"skip this one, keep going" — loop continues. Good for filters (skip iterations that don't qualify). All qualifying iterations are processed.break:"found it, stop looking" — loop exits. Good for searches (find first match). Only the first qualifying iteration is processed. The bug happens when you use continue thinking "skip the cases I don't want" but forget the loop still runs all remaining iterations, overwriting the output each time a qualifying case is found.

💡 Senior Verification Engineer Tip: Run Lint on Control Flow Keywords

Most RTL lint tools (Spyglass, Synopsys Lint, Cadence JasperGold) have rules specifically for these control flow keywords: detecting break/continue in non-synthesizable loops, flagging missing return paths in functions, and checking disable label matches. Enable these rules and set them to ERROR severity during RTL sign-off. A function with a missing return path is as dangerous as a latch from missing default — both produce undefined values on untested code paths.

🎯 Interview Q&A — Control Flow in RTL and Verification

Beginner Level

BeginnerWhat is the difference between break and continue?break: exits the innermost loop immediately. Execution continues with the statement after the loop's end. No further iterations occur.continue: skips the remainder of the current iteration and jumps to the next iteration check. The loop itself continues running. Analogy: searching a list for an item — break is "found it, stop searching." continue is "not this one, try next." Key danger: break inside a forever loop kills that process permanently. continue inside forever without a timing control creates a zero-time infinite loop. Both are common beginner mistakes.BeginnerWhat is the difference between return in a function vs a task?In a function: return expr; exits the function and returns the value expr as the function's output. Functions are synthesizable, so return inside synthesizable functions is also synthesizable — each return path generates a mux selecting which value drives the output. In a task: return; exits the task with no value (tasks cannot return values directly — they use output ports instead). Task return is synthesizable only if the entire task is synthesizable (no timing controls, constant loop bounds). Important: always clean up any driven signals (deassert valid, ready, etc.) before calling return from a task — otherwise the DUT is left in an unknown state.BeginnerIs disable synthesizable? When should you use it?disable is not synthesizable. It relies on the simulation process model — the concept of "killing a running thread" does not exist in gate-level hardware. Use disable in verification only, in two scenarios: 1. disable label; — to exit a named begin-end block from inside, similar to break but works for arbitrary named blocks, not just loops. 2. disable fork; — to kill all child threads spawned by the current process. Essential for the timeout watchdog pattern: fork test_body watchdog join_any; disable fork; Prefer break over disable label when you simply need to exit a loop — break is more readable and cannot have a label typo.

Intermediate Level

IntermediateWhat hardware does break produce when synthesis unrolls a for loop?Synthesis generates a "found" flag that propagates through the unrolled iterations. Once the break condition is met, the flag is set and gates all subsequent iterations — preventing them from updating the output. For example, a priority encoder with break: Iteration 0: if(!found && req[0]) {out=0; found=1;} Iteration 1: if(!found && req[1]) {out=1; found=1;} ... The synthesized hardware is a cascade of AND gates checking the "found so far" condition — this is a standard priority encoder tree. The break in RTL produces identical hardware to writing cascaded if-else if — the choice is purely for code readability.IntermediateExplain the fork-join_any + disable fork timeout watchdog pattern.This is the standard production testbench pattern for preventing simulation hangs: 1. fork spawns two parallel threads: the test body and a watchdog timer. 2. join_any waits until either thread finishes — whichever finishes first. 3. disable fork kills all remaining child threads (whichever one didn't finish). If the test finishes first: the watchdog is killed, test result is reported, simulation ends normally. If the watchdog fires first: the test body is killed (it was hanging), $fatal is called, simulation aborts with a timeout message. Key subtlety: disable fork kills ALL child threads of the current process — not just the watchdog. If you have additional concurrent threads (coverage sampler, protocol monitor) that should continue running, use named labels (begin : label_name) and disable specific_label instead of the broad disable fork.IntermediateA loop that uses continue to skip invalid data also uses += to accumulate. What hardware does this produce?The synthesis tool treats continue as a conditional gate on the loop body. The unrolled hardware for:if(!valid[i]) continue; sum += data[i]; is equivalent to:if(valid[i]) sum += data[i]; In the netlist: each data[i] is ANDed with valid[i] before entering the adder tree. Invalid bytes contribute 0 to the sum (because data[i] & 0 = 0). The hardware is a masked adder — identical to what you'd generate by writing the conditional directly. This means continue in always_comb is purely a code-readability choice in synthesis — it produces no extra logic compared to the equivalent if(!skip) formulation.

Advanced / Debugging Level

AdvancedYou have a nested loop in RTL where break exits the inner loop, but you need to exit both loops simultaneously. What are the two correct approaches?Approach 1: Found flag in outer loop condition Declare a bit variable found. Set it inside the inner loop before break. Add && !found to the outer loop condition: for(int r=0; r<N && !found; r++). After the inner loop's break, the outer loop checks !found, sees it's false, and terminates.Approach 2: disable label on outer loop scope Wrap both loops in a named block: begin : outer_search ... end. Inside the inner loop, use disable outer_search; instead of break. This exits the entire named block immediately, skipping the remainder of both loops.In RTL synthesis: Approach 1 (found flag) is preferred — it's synthesizable and reads clearly. Approach 2 (disable label) is simulation-only and not synthesizable.In verification testbenches: either works. Approach 2 is more concise for complex multi-level exits.AdvancedA lint warning says "function may return without a value on some code path." Why is this dangerous and how do you fix it?When a function reaches the end without executing a return statement, it returns the current value of the implicit function return variable — which was never explicitly set. In 2-state simulation, this is 0. In 4-state simulation (with X-propagation enabled), this may be X. Why it's dangerous: (1) In 2-state simulation, returning 0 may accidentally match the expected value for specific test inputs — the test passes when it should fail. (2) In synthesis, the tool generates a mux for each return path, but the "no return" path drives an unconnected input — undefined behavior. (3) Between simulators, 2-state vs 4-state interpretation differs — the same code may pass in VCS and fail in Questa. Fix: every code path must end with an explicit return value;. Add a final return default_value; at the end of the function as a safety net even if you believe all paths are covered. Enable the lint rule that flags this at ERROR severity — never suppress it.