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.
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.
// 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
endWhere 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.
// 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.