Skip to content

SystemVerilog · Module 15

Program Block vs Module — Race-Free Simulation

SystemVerilog program block — the construct that solves the RTL-testbench race by running in the Reactive region (after Active + NBA). Why the race exists, how the scheduler ordering eliminates it, the constructs program forbids (always, clock generators), the $exit vs $finish discipline, and how UVM moved past program blocks while keeping their semantic intent.

Module 15 · Page 15.1

When a testbench module and the RTL share the same clock edge, they compete for the same scheduler region. The result is a race condition — and the winner is decided by simulator evaluation order, not by design intent. The program block was added in SystemVerilog precisely to eliminate this race by moving testbench code into its own scheduler region — Reactive, which runs strictly after the RTL's Active and NBA regions. By the time the program block's code executes, every flip-flop in the DUT has settled to its final value for the current clock edge. The race is structurally impossible because RTL and testbench are in different, ordered scheduling regions.

1. Engineering Problem — The Active-Region Race

In Verilog and pre-program SystemVerilog, both RTL and testbench code run in the Active region of the simulator scheduler. When a clock edge arrives, both the flip-flop update logic (always_ff @(posedge clk) q <= d;) and the testbench sampling code (@(posedge clk); read_q();) try to execute in the same scheduler region — with no guaranteed ordering between them.

If the testbench reads a signal before the flip-flop has updated it at the same time step, it sees the old value. If it reads after, it sees the new value. Which one it sees depends entirely on the simulator's internal evaluation order — different tools, different runs, and even different optimisation flags can give different results.

The race is not a bug you wrote. It is a structural property of having both RTL and testbench in the Active region. No amount of careful coding inside the testbench can fix it; the only reliable solution is to move the testbench out of the Active region entirely. That is exactly what program does.

2. Mental Model — The Reactive Region Is Where the Dust Settles

The picture every engineer carries:

A SystemVerilog time step is a fixed sequence of scheduling regions: Preponed → Active → Inactive → NBA → Observed → Reactive → Postponed. RTL flip-flop updates commit in NBA (non-blocking assignments). Module testbench code wakes in Activebefore NBA — so it sees pre-edge values. A program block's initial blocks run in Reactiveafter NBA — so they see the fully-settled post-edge values. The dust has settled before the program block opens its eyes.

Three invariants this picture preserves:

  • The region ordering is non-negotiable. IEEE 1800 §4.4 defines the region sequence; every conformant simulator must respect it. The race-elimination is therefore a language guarantee, not a coincidence of one simulator's implementation.
  • program is for testbench, not RTL. Modules host clocks, instantiate sub-modules, run always_ff. Programs host initial and final blocks only — they cannot contain always, always_ff, always_comb, clock generators, or module instances. The constraints are intentional: the program block is the testbench control layer, not the design.
  • Writes from a program schedule to next-Active. Program-block assignments to DUT inputs are scheduled to the next time step's Active region — modelling the natural setup-before-edge timing of synchronous interfaces. Reads in Reactive, writes for next Active: the program block is half-cycle-shifted from the RTL.

3. Visual Explanation — Where program Code Lives

The seven scheduling regions of a single time step, with program's home region (Reactive) highlighted:

Program block initial / final code runs in the Reactive region

event regions
Program block initial / final code runs in the Reactive regiontime slot tPreponed$monitor samplingActiveblocking assignments, RHS samplingInactive#0 scheduled eventsNBAnon-blocking LHS updatesObservedPLI / SVAReactiveprogram block codePostponed$strobe sampling
Within one simulation time step, the regions run in fixed order: Preponed → Active → Inactive → NBA → Observed → Reactive → Postponed. RTL non-blocking assignments commit in NBA. Module testbench code wakes in Active — BEFORE NBA — so it sees pre-edge values (the race). Program block code wakes in Reactive — AFTER NBA — so it always sees the settled post-edge values.

The teaching is in the visual ordering. Active and NBA are where RTL lives; Observed hosts SVA sampling; Reactive is where program-block testbench code wakes. The horizontal arrow shows the irreversible left-to-right time-flow within one time step — by the time Reactive runs, NBA has already committed every non-blocking assignment from the current edge.

4. Syntax & Semantics — program, Ports, $exit

4.1 The basic form

SystemVerilog — program block syntax and port list
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
program test_name (
    input  logic       clk,
    input  logic       rst_n,
    output logic       req,
    input  logic       ack,
    input  logic [7:0] data_out
);
 
    initial begin
        // This initial block runs in the REACTIVE region.
        // All signal reads here see post-NBA (fully settled) values.
        req = 0;
        @(posedge clk);              // wait for clock edge
        // By the time we're here (Reactive), the DUT has already committed
        // all non-blocking assigns. data_out and other outputs are stable.
        req = 1;                      // drive output — schedules to next Active
        @(posedge clk iff ack);       // wait until ack
        assert (data_out !== 8'hX);   // safe: data_out settled in NBA
        $display("[TEST] data_out = 0x%h", data_out);
        $exit;                        // graceful end of this program
    end
 
    final
        $display("[TEST] Program block exiting");
 
endprogram

A program block has a port list, can connect to interfaces, contains initial and final blocks, and is instantiated from a module like any other unit. The differences from module are in what's allowed inside and which scheduler region the code runs in.

4.2 What program forbids — and why

ConstructAllowed in program?Why
initial blockThe primary structural element — runs in Reactive
final blockEnd-of-simulation cleanup / reporting
Tasks, functions, classesStandard SV building blocks
always, always_ff, always_comb, always_latchRun continuously; belong in modules, not in test programs
Clock generator (always #5 clk = ~clk)A clock is shared infrastructure for RTL; must live in a module
Module instantiationProgram is the test consumer, not a design hierarchy node
interface instantiation✓ via portConnect by port to interfaces declared in the top module

The constraints are deliberate: a program block is a testbench control program, not a piece of RTL infrastructure. Anything that runs forever (clocks, always blocks, monitors that fire on every edge) belongs in modules; the program orchestrates the test by waiting on clock edges and driving / sampling DUT signals.

4.3 $exit vs $finish

SystemVerilog — $exit vs $finish for program-block termination
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── $exit: graceful — ends this program; sim ends when ALL programs exit
program test_a (input clk);
    initial begin
        run_test();
        $exit;      // lets other programs' final blocks run cleanly
    end
endprogram
 
// ── Implicit $exit: the last initial block completing exits the program
program test_b (input clk);
    initial begin
        run_test();
        // When this initial ends naturally, the program exits.
        // No $exit needed if it is the only initial block.
    end
endprogram
 
// ── $finish: abrupt — kills ALL simulation immediately
program test_c (input clk);
    initial begin
        run_test();
        $finish;    // halts everything; other programs' final blocks may not run
    end
endprogram

Default to $exit inside a program block. The language treats program termination as a clean shutdown — when every program block has exited, simulation ends. $finish is the nuclear option; reserve it for unrecoverable errors. The discipline parallels the $finish race from Module 14.3: kill background work cleanly or lose end-of-test data.

5. Simulation View — How the Scheduler Eliminates the Race

The IEEE 1800-2017 region order for a single time step is fixed: Preponed → Active → Inactive → NBA → Observed → Reactive → Postponed. The race-elimination property of program falls out of this ordering in three steps:

  1. RTL flip-flop updates use non-blocking assigns. always_ff @(posedge clk) q <= d; schedules q = d into the NBA region of the current time step. The flip-flop output q does not change in Active; it changes in NBA.
  2. Module testbench code wakes in Active. A module initial block with @(posedge clk); wakes when the clock-edge event is delivered in the Active region. At this moment, q still holds its pre-edge value — NBA has not run yet, so the assignment from step 1 is pending but not committed.
  3. Program testbench code wakes in Reactive. A program initial block with @(posedge clk); wakes in the Reactive region — after Active and after NBA. The assignment from step 1 has committed; q now holds its post-edge value. The program block sees the settled value.

The race in a module testbench is therefore: "Active runs Active-code in some order; if RTL's <= evaluates before testbench's read, the schedule queues an NBA update and the testbench reads pre-edge q; if testbench's read evaluates before RTL's <=, the testbench still reads pre-edge q." Either way the module-tb reads pre-edge q. The program-tb cannot make this mistake — Reactive is strictly after NBA.

Writes from a program block are subtler: a program-block assignment to a DUT input does not take effect in the current Reactive region. It is scheduled to the Active region of the next time step. This models the natural setup-before-edge stimulus pattern of synchronous interfaces.

6. Waveform — Module Reads Pre-Edge, Program Reads Post-Edge

The waveform below shows the race in motion. The DUT registers d into q on every posedge clk. A module-based testbench reads q at the same posedge clk and sees the pre-edge q — wrong. A program-based testbench wakes in Reactive (later in the same time step) and sees the post-edge q — correct.

Figure — module-tb reads pre-NBA q (RACE); program-tb reads post-NBA q (CORRECT)

10 cycles
Figure — module-tb reads pre-NBA q (RACE); program-tb reads post-NBA q (CORRECT)posedge clk: NBA commits q=1; module-tb already read q=0 (race!)posedge clk: NBA commi…Reactive: program-tb reads q=1 (correct)Reactive: program-tb r…clkdq (NBA commit)module_tb readsq=0q=0q=0q=0q=0q=0q=0q=0program_tb readsq=1q=1q=1q=1q=1q=1q=1q=1t0t1t2t3t4t5t6t7t8t9
At cycle 2 the DUT registers d=1 into q via non-blocking assign. Both testbenches wake at this clock edge. Module-tb (Active region) reads q=0 — the pre-edge value, because NBA has not run yet. Program-tb (Reactive region) reads q=1 — the post-NBA settled value. Same time step, same edge, different scheduling region → completely different result.

Both annotations sit at cycle 2 because both testbenches wake at the same $time. They differ only by scheduling region within that time step — module-tb in Active (before NBA), program-tb in Reactive (after NBA). The trace surfaces what the prose says: same edge, different region, opposite result.

7. Synthesis — Not Applicable

program is a procedural simulation construct. It has no hardware footprint and no place in synthesisable RTL — synthesis tools reject the program keyword outright. This section is intentionally omitted; the topic does not warrant it.

The corresponding hardware-side discipline for "read settled values, not in-flight values" is to use registered outputs that have already propagated through the flip-flop one cycle earlier. That is RTL design discipline, not a procedural primitive.

8. Verification View — Connecting via Interface + Clocking Block

The standard production pattern is to wire program blocks to DUTs through an interface that carries a clocking block. The interface bundles the signals; the clocking block defines the testbench sampling point (with explicit input/output skews — covered in 15.3 (coming)). The program block accesses signals through the clocking block, getting both the Reactive-region timing and the clocking-block skew controls in one composition.

SystemVerilog — program block connected through an interface + clocking block
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Interface: all DUT signals bundled together ───────────────
interface apb_if (input logic pclk);
    logic        presetn, psel, penable, pwrite, pready, pslverr;
    logic [31:0] paddr, pwdata, prdata;
 
    // Clocking block defines the testbench sampling point.
    clocking cb @(posedge pclk);
        default input #1 output #1;   // skews — see 15.3
        input  pready, pslverr, prdata;
        output psel, penable, pwrite, paddr, pwdata;
    endclocking
 
    modport TB   (clocking cb, output presetn);
    modport DUT  (input psel, penable, pwrite, paddr, pwdata, presetn,
                  output pready, pslverr, prdata);
endinterface
 
// ── Program block: drives + samples via the clocking block ────
program apb_test (apb_if.TB bus);
 
    initial begin
        bus.presetn = 0;
        repeat(4) @(bus.cb);             // wait 4 clocking-block edges
        bus.presetn = 1;
 
        bus.cb.psel    <= 1;             // applied with output skew
        bus.cb.paddr   <= 32'h1000;
        bus.cb.pwrite  <= 1;
        bus.cb.pwdata  <= 32'hCAFE;
        @(bus.cb);
 
        bus.cb.penable <= 1;
        @(bus.cb iff bus.cb.pready);     // wait until pready — sampled with input skew
        bus.cb.psel    <= 0;
        bus.cb.penable <= 0;
 
        $display("[TEST] Write complete at %0t", $time);
        $exit;
    end
 
endprogram
 
// ── Top level wires everything together ───────────────────────
module top;
    logic pclk = 0;
    always #5 pclk = ~pclk;              // clock MUST be in a module
 
    apb_if    bus (.pclk(pclk));
    apb_slave dut (.bus(bus.DUT));
    apb_test  tb  (.bus(bus.TB));
endmodule

This composition — program block + interface + clocking block — was the canonical race-free SV testbench pattern from 2005 to roughly the UVM 1.0 era. Modern UVM testbenches typically use modules (not programs) for the test wrapper and rely on clocking blocks alone to provide the race-free sampling discipline — see §9 industry usage for why.

8.1 module vs program — side-by-side comparison

Featuremoduleprogram
Scheduler regionActive + NBA — same as RTLReactive — after all RTL updates
Race condition with RTL?Yes — scheduling-order dependentNo — structurally impossible
always blocks allowed?Yes (always, always_ff, always_comb)No — only initial and final
Clock generatorsYes — must live hereNo — use a module
Sub-module instantiationYesNo — cannot instantiate modules
Terminates simulation?No — runs until $finishYes — last initial exiting triggers sim end
Used forRTL, DUT, clocks, interfaces, bus-functional modelsTest programs, directed / random stimulus, checker logic

9. Industry Usage — Why Modern UVM Moved Past program

program was added to SystemVerilog 2005 to bring Vera / OpenVera / Specman E heritage into the language — those proprietary HVLs had separate scheduling regions for testbench code, and program was the SV mechanism for the same race-elimination. For the first generation of SV verification environments (2005–2010), program blocks were the canonical test wrapper.

Then UVM happened. UVM 1.0 (2011) and onward standardised on module-based test wrappers (uvm_test is a class instantiated inside a module, not a program). Race-free sampling is achieved instead via clocking blocks in interfaces — the same Reactive-region timing the program block provided, but available to module-based code without needing the program construct.

Why the shift? Three reasons:

  • UVM needs forever / always for monitors. UVM uvm_monitor::run_phase() contains forever loops sampling DUT outputs — which a program block cannot host (only initial and final are allowed inside program). The UVM architecture needs module-hosted continuous activity.
  • One scheduling discipline beats two. Mixing modules and programs in the same testbench creates two scheduling models the verification engineer must reason about. Standardising on modules + clocking blocks gives one model.
  • $exit semantics confuse multi-test regression. Program-block $exit semantics interact poorly with regression harnesses that run multiple tests per simulation invocation. UVM's uvm_phase machinery (objections, drain time) replaces program-block lifecycle with finer-grained control.

program is still in the language, still race-free, still occasionally seen in legacy and pedagogical contexts. New testbench architectures default to module wrappers + clocking blocks instead. The race-elimination semantics that program introduced now live in clocking blocks, available to any context — see 15.2 Clocking Block Deep Dive for the construct that does the heavy lifting in modern UVM.

10. Design Review Notes — What a Senior Will Flag

Pattern in the diffWhat review will say
program test; always #5 clk = ~clk; endprogram"Compile error — always is forbidden inside program. Clock generators live in modules; pass clk into the program through its port list."
program test; ... $finish; endprogram"Use $exit for program termination. $finish is the nuclear option that kills every still-running program / module immediately; $exit lets other programs' final blocks run cleanly."
program test; apb_slave dut(...); endprogram"Cannot instantiate modules inside program. Instantiate the DUT in the top-level module; pass it to the program through an interface."
program test (input clk, output req); initial begin @(posedge clk); req = 1; end endprogram"Driving outputs directly from program-block code without a clocking block produces zero-setup-time pulses at the next Active region. Use a clocking block with an output skew so the DUT sees stimulus with realistic setup time. See 15.2 / 15.3."
module tb; program inner; ... endprogram inner i(); always_ff @(posedge clk) check(); endmodule"Mixing module-hosted assertion / check logic with program-hosted testbench reintroduces the race for the module-side code. If you commit to a program for testbench race-elimination, all sampling must go through the program (or through clocking blocks). Half-program is half-fixed."
New UVM testbench using program for the test wrapper"UVM convention is module wrapper with clocking blocks for race-free sampling. program is legacy / pre-UVM. Most lint flows flag program-blocks-in-UVM-environments as an anti-pattern."
program test without a port list, accessing top-level signals via hierarchical reference"Hierarchical signal access bypasses the interface discipline. Even though program block reads in Reactive are race-free, hierarchical accesses make the testbench tightly coupled to one DUT structure. Use ports + interfaces."
final begin $display("FAIL"); end inside a program with no actual check"A final block is for end-of-sim reporting, not for deciding pass / fail. Move the check into the initial body's main flow; use final only for summary output."

The single highest-value rule: understand which scheduling region the testbench code runs in. program makes it explicit (Reactive); clocking blocks make it explicit (post-NBA per the block's clock); pure-module testbench code is the dangerous default (Active, racy). The discipline question for every testbench review is: "how is this sampling race-free?" — if the answer isn't "program block" or "clocking block," you have a race.

11. Debugging Guide — Real Failures, Real Fixes

1

Module testbench reads q=0 right after DUT registers d=1

ACTIVE-REGION-RACE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb;
    logic clk = 0, d = 0, q;
    dff dut (.clk, .d, .q);
    always #5 clk = ~clk;
 
    initial begin
        d = 1;
        @(posedge clk);
        $display("q = %b (expected 1)", q);   // prints 0 — RACE
    end
endmodule
Symptom
Test prints q = 0 (expected 1) even though the DUT clearly registered d=1 on the same posedge clk. Behaviour is simulator-dependent: VCS may print 0, Questa may print 1, Xcelium may print one then the other across versions. Cannot be debugged by inspecting the trace — Verdi shows q updating at the right edge.
Root Cause
The Active-region race. Both the DUT's always_ff @(posedge clk) q <= d; and the testbench's @(posedge clk); wake in the Active region. The DUT's <= schedules an NBA-region update; the testbench's $display reads q before NBA runs. The testbench sees pre-edge q = 0.
Fix
Move the testbench into a program block. The program's initial runs in Reactive — after NBA — so q is settled by the time $display runs. Or, in a module-based testbench, sample through a clocking block (see 15.2). Either way the structural region separation eliminates the race.
2

Compile error: 'always' not allowed inside program

ALWAYS-IN-PROGRAM
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
program bad_test;
    logic clk = 0;
    always #5 clk = ~clk;          // ERROR
    initial begin /* ... */ end
endprogram
Symptom
Compilation fails with "always block not allowed inside program" from VCS / Questa / Xcelium. Confusing because always is the bread-and-butter Verilog construct.
Root Cause
Program blocks are restricted to initial and final blocks — by design. always runs continuously; it belongs in module-hosted infrastructure, not in a test program. The compile error is the language enforcing the architectural rule.
Fix
Move the clock generator to the top-level module: module top; logic clk = 0; always #5 clk = ~clk; ... program_inst tb(.clk); endmodule. Pass clk into the program through its port list. The program block now sees the clock and waits on it; the clock generator runs in the module where always is allowed.
3

Other programs' final blocks never run

FINISH-INSTEAD-OF-EXIT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
program test_a;
    initial begin
        run_directed_test();
        $finish;                   // BUG: kills test_b before its final runs
    end
endprogram
 
program test_b;
    initial run_random_test();
    final $display("[test_b] coverage = %0d%%", get_coverage());
endprogram
Symptom
test_a reports complete. test_b's coverage summary never prints. Regression log is missing the per-test coverage data. Engineer assumes test_b crashed or didn't run.
Root Cause
$finish in test_a aborted the entire simulation immediately — including test_b and its final block. $finish does not allow other programs' final blocks to execute. The expected graceful shutdown never happened.
Fix
Use $exit instead of $finish inside program blocks. $exit ends only this program; the simulator continues running other programs (which is what allows their final blocks to fire). Simulation ends naturally when every program has exited. Reserve $finish for unrecoverable errors where you genuinely don't want any cleanup.
4

DUT sees req=1 in the same time step the program drove it — setup time = 0

NO-CLOCKING-BLOCK-SKEW
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
program test (input clk, output logic req);
    initial begin
        @(posedge clk);
        req = 1;        // assigned in Reactive
                        // scheduled to next Active at the SAME-time-step delta
                        // DUT sees req=1 with effectively zero setup time
    end
endprogram
Symptom
Coverage shows the protocol's req → ack handshake completing at the same time as the request — sometimes. Simulator timing report flags zero-cycle setup. Real hardware would never produce this stimulus shape; the testbench is non-physical.
Root Cause
Direct assignments from program-block code schedule the new value to the next time step's Active region — but in the same cycle as the @(posedge clk). The DUT effectively sees the input change in the same time step, simulating zero setup time. Real synchronous interfaces always have one-cycle setup.
Fix
Drive through a clocking block: bus.cb.req <= 1;. The clocking block's output #N skew shifts the actual signal change N time units after the clock edge, modelling the realistic setup-time relationship the DUT expects. Full discipline covered in 15.2 Clocking Block Deep Dive.
5

Half the testbench is race-free, half is racy — debug nightmare

MIXED-MODULE-PROGRAM
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb;
    program inner;
        initial begin @(posedge clk); check_in_program(); end
    endprogram
    inner i ();
 
    always_ff @(posedge clk) check_in_module();   // STILL HAS RACE
endmodule
Symptom
The check_in_program() calls always see correct DUT outputs. The check_in_module() calls intermittently see pre-edge values — failing 5% of the time across seeds. Coverage looks fine; assertion failures are clustered in the module-hosted check, not the program-hosted one.
Root Cause
Putting some testbench code in a program block doesn't help the module-hosted code. The module's always_ff @(posedge clk) still wakes in Active, still reads pre-edge values, still races with the DUT. The program-block fix is structural — it has to be applied to every testbench sampling point.
Fix
Either move every testbench sampling site into the program block (or another program block), or use a clocking block to give the module-hosted check the same Reactive-region timing the program provides. Half-fixes leave half the race. The discipline is "all testbench reads go through one of two race-free mechanisms" — never module-Active.

12. Interview Insights — What Interviewers Actually Probe

The Active-region race: a module-based testbench's @(posedge clk) wakes in the Active region — before the NBA region commits the DUT's flip-flop updates from that edge. The testbench reads pre-edge signal values; the DUT thinks it has updated them. Which order resolves depends on simulator evaluation order — non-deterministic.

A program block's initial runs in the Reactive region — strictly after Active and NBA. By the time program code executes, every non-blocking assignment from this clock edge has committed. The testbench reads post-edge settled values. The race is structurally impossible because RTL (Active+NBA) and testbench (Reactive) are in different, ordered regions.

13. Exercises

1. Design — race-free APB writer (Foundation)
Build a program apb_writer (apb_if.TB bus); that performs three APB writes to different addresses through bus.cb (the clocking block). Confirm the writes are visible to the DUT one cycle after they are issued (the clocking block's output-skew behaviour).

2. Debug — the silent race (Intermediate)
A teammate's module testbench reads q immediately after posedge clk and sometimes sees the pre-edge value. Same code, same DUT, different result across simulators. Identify the race, walk through which scheduling regions cause it, and propose two distinct architectural fixes (one moving the testbench into a program, one keeping it in the module but using a clocking block).

3. Code review — the dirty program (Intermediate)
A teammate submits a program test; containing always #5 clk = ~clk;, a module instantiation, and a call to $finish. Identify all three architectural violations and write the canonical fixes (one fix per violation).

4. Trade-off — program vs module + clocking block (Advanced)
A junior teammate proposes a coding rule: "always use program for testbench code — it's the canonical SV race-free construct." Argue the case against the blanket rule. When does module + clocking block strictly beat program? Consider the requirements of UVM monitors (forever loops), multi-test regression harnesses, and architectural-consistency concerns.

14. Summary

program runs in the Reactive region — strictly after Active and NBA. RTL flip-flop updates have already committed by the time program code executes, so the program always reads post-edge settled values. The Active-region race that plagues module-based testbench sampling is structurally impossible inside a program block.

Defaults to memorise. Programs allow only initial and final — no always, no clock generators, no sub-module instantiation. Use $exit for graceful program termination; reserve $finish for unrecoverable errors. Drive DUT inputs through a clocking block (with output skew) to model realistic setup-time stimulus. Program blocks are legacy/pedagogical in modern UVM — production testbenches use module + clocking block for the same race-free semantics with broader architectural compatibility.

Next up: 15.2 — Clocking Block Deep Dive (coming next) — the construct that gives modern module-based UVM testbenches the same Reactive-region timing the program block introduced, plus explicit input/output skew control, default direction conventions, and multi-clock support.