Part 1 · Language Foundations · Intermediate

The Event Scheduler & Stratified Regions

Active/Inactive/NBA/Observed/Reactive/Postponed regions, #0 abuse, where assertions sample, and why program and clocking blocks exist.

The stratified event queue

Within a single simulation time step, events are not one flat queue — the LRM stratifies them into ordered regions , and a time step may iterate through them repeatedly until no events remain. The ones that matter day-to-day: Active (blocking assignments, RHS evaluation of nonblocking assignments, continuous assignments), Inactive (everything postponed by #0), NBA (the left-hand-side updates of nonblocking assignments), Observed (concurrent assertion evaluation), Reactive (program block code and assertion action blocks), and Postponed ($strobe, $monitor, final value sampling). Understanding which region your code runs in is the difference between explaining a race and being bitten by one.

diagram
ONE TIME SLOT — STRATIFIED REGIONS (simplified)

          ┌────────────────────────────────────────────┐
   from   │  Preponed   sample values for assertions   │
   prev   │     │       (stable pre-slot snapshot)     │
   slot   ▼     ▼                                      │
  ┌──────────────────┐                                 │
  │     ACTIVE       │  blocking =, eval RHS of <=,    │
  │                  │  continuous assigns, $display   │
  └────────┬─────────┘                                 │
           ▼                  ▲ iterate back if new    │
  ┌──────────────────┐        │ events appear          │
  │    INACTIVE      │  #0-delayed statements ─────────┤
  └────────┬─────────┘                                 │
           ▼                                           │
  ┌──────────────────┐                                 │
  │       NBA        │  commit LHS of <= updates ──────┤
  └────────┬─────────┘                                 │
           ▼                                           │
  ┌──────────────────┐                                 │
  │    OBSERVED      │  evaluate concurrent assertions │
  └────────┬─────────┘                                 │
           ▼                                           │
  ┌──────────────────┐                                 │
  │    REACTIVE      │  program blocks, assertion ─────┘
  │                  │  pass/fail action code
  └────────┬─────────┘
           ▼
  ┌──────────────────┐
  │    POSTPONED     │  $strobe / $monitor: final values
  └────────┬─────────┘
           ▼  advance to next time slot

#0: what it does and why it spreads

A #0 delay moves the rest of the statement into the Inactive region — “run me again after the current Active events drain, but still at this time”. Engineers discover it as a quick fix for an ordering race (“my read ran before the write — add #0”) and it appears to work. The problem: it only loses to other Active-region code once . When two processes both use #0 you are back to unordered execution one region later, so the next fix is #0 #0 — an arms race that makes execution order unexplainable. The robust orderings are structural: nonblocking assignments (commit in NBA, after all sampling), event handshakes, or moving TB code into the Reactive region via program blocks/clocking blocks.

systemverilog
// RACE: same time, two processes, unordered
initial data  = compute();       // writer
initial $display("got %0d", data); // reader — may see old value

// FRAGILE fix: hop one region and hope nobody else does
initial begin
  #0 $display("got %0d", data);  // now in Inactive — wins... until
end                              // the writer also adds #0

// ROBUST fix: read committed NBA values at the next edge
always_ff @(posedge clk) data <= compute();
always   @(posedge clk) begin
  @(posedge clk);                 // or sample via clocking block
  $display("got %0d", data);     // sees the committed value
end

Where assertions sample — and why program/clocking blocks exist

Concurrent assertions sample their expressions in the Preponed region — a read-only snapshot taken before anything in the slot executes — and evaluate in Observed , after NBA commits. So an assertion at a clock edge always sees the values just before that edge: stable, race-free, and exactly what a real flop would have sampled. This is why an assertion can never race with the RTL it watches. The same root problem — TB code racing DUT code inside the Active region — is what program blocks solve structurally: their code runs in Reactive , after the design has settled and assertions have evaluated, so the TB reads final post-edge values and its drives take effect cleanly. Clocking blocks finish the job at the signal level, defining sampling skews (read just-before-edge values) and driving skews (drive after the edge) so TB-DUT timing is correct by construction rather than by #0 folklore. In modern UVM flows, class-based TBs plus clocking blocks largely replace program blocks, but interviewers still expect you to explain the region motivation behind both.

systemverilog
// Assertion: samples in Preponed → pre-edge values, no race
property req_then_gnt;
  @(posedge clk) disable iff (!rst_n)
    req |-> ##[1:3] gnt;
endproperty
assert property (req_then_gnt)
  else $error("grant missed window");   // action runs in Reactive

// Clocking block: region-safe TB sampling and driving
interface bus_if (input logic clk);
  logic req, gnt;
  clocking cb @(posedge clk);
    default input #1step output #2ns;  // sample pre-edge, drive post-edge
    input  gnt;
    output req;
  endclocking
endinterface

// TB usage: vif.cb.req <= 1'b1;  @(vif.cb);  if (vif.cb.gnt) ...

Key takeaways

  • Region order per slot: Active → Inactive(#0) → NBA → Observed → Reactive → Postponed, with iteration.

  • #0 hops one region and 'fixes' races until the other side also uses it — prefer NBA, events, or clocking blocks.

  • Assertions sample in Preponed (pre-slot snapshot) and evaluate in Observed — they cannot race the RTL.

  • program blocks run TB code in Reactive; clocking blocks encode sample/drive skews — both exist to kill TB-DUT races.

Common pitfalls

  • Stacking #0s to win ordering battles — execution becomes order-dependent folklore nobody can explain.

  • Using $display to debug NBA values — it runs in Active and prints pre-commit values; $strobe shows final ones.

  • Expecting an assertion to see a value assigned with <= at the same edge — it sampled before the slot began.

  • Driving DUT inputs from plain initial blocks at the active clock edge instead of via clocking blocks.