SystemVerilog · Module 15
Program Block Limitations & Best Practices
SystemVerilog program block limitations and the industry's shift to module + clocking block. What is illegal inside a program (always, modules, nets), why UVM doesn't use program blocks, the modern module + clocking block pattern that achieves race-free simulation without program's restrictions, and the decision guide for when each is appropriate.
Module 15 · Page 15.4
The program block (15.1) solves the RTL-testbench race condition cleanly — but it comes with real restrictions that limit how testbenches are structured. Equally important: the industry has largely moved away from program blocks in favour of module-based testbenches that use clocking blocks (15.2) directly. UVM — the dominant verification methodology — doesn't use program blocks at all. Understanding the limitations and the modern alternative is what lets you work in any codebase you encounter: read and maintain legacy program-based tests, write new code in the module + clocking block pattern, and explain the architectural choice in design reviews and interviews. This page closes Module 15 with the operational discipline that the prior three pages established the theoretical foundation for.
1. Engineering Problem — Limitations Force an Architectural Choice
program was introduced in SystemVerilog 2005 to solve the TB-DUT race. It did — by forbidding every construct that could violate the testbench-is-pure-stimulus model:
- No
alwaysblocks (noalways,always_ff,always_comb,always_latch). A program is meant to terminate;alwaysruns forever and belongs in modules. - No module / UDP instantiation. The DUT lives in modules; the program is the test consumer that uses the DUT, not the parent that builds it.
- No
wire/trinet declarations. Programs are pure stimulus + check; net resolution belongs to the hardware-modelling side. - No
specifyblocks, no analog/mixed-signal, nogenerateblocks. All of these are RTL or elaboration-time constructs.
These restrictions are the price of admission for the Reactive-region race-free guarantee. The price is high enough that virtually no production verification environment today uses program blocks — including every shipping UVM testbench, every major commercial VIP, and every shop's regression infrastructure.
What replaced program blocks: modules + clocking blocks. A clocking block inside an interface gives the testbench Reactive-region sampling — same race elimination as program — without the program's structural restrictions. The clocking block was always the load-bearing construct; the program block was just a wrapper that forced its use.
The architectural decision today is therefore: understand program blocks conceptually, be able to maintain legacy code that uses them, but write new testbenches as modules with clocking-block-driven interfaces.
2. Mental Model — The Clocking Block Is the Key Insight, Not the Program Block
The picture every engineer carries:
The race-elimination property doesn't come from
program— it comes fromclocking. A clocking block declared in an interface and accessed via@(cb)and<=samples in the Observed / Reactive regions whether the calling code is inside aprogramblock or inside amodule'sinitialblock. The program block adds the extra guarantee that all code in the program runs in Reactive — not just clocking-block accesses. That matters only for testbenches that access DUT signals directly (without going through a clocking block). If you always go through the clocking block (which UVM and every modern VIP does), the program block adds no value.
Three invariants this picture preserves:
- Clocking blocks work in modules.
module tb; initial begin @(bus.cb); bus.cb.psel <= 1; end endmoduleis exactly as race-free as the same code wrapped in aprogram. The clocking block is what creates the timing contract; the surroundingmodulevsprogramis a structural choice. moduleenables whatprogramforbids. Modules can host clock generators (always #5 clk = ~clk;), instantiate sub-modules (the DUT, the interface itself), declare nets — all the things a real testbench actually needs.programcannot.- Architectural cost beats theoretical safety. The "extra Reactive guarantee"
programprovides for non-clocking-block signal access is value only if you bypass the clocking block. The right discipline is "never bypass the clocking block," which removes the need for program-block protection.
3. Visual Explanation — Legal vs Illegal Constructs Inside program
The structural restrictions are the architectural argument. The two tables below summarise what the language allows and forbids inside a program block.
3.1 Legal inside program
| Construct | Why it's allowed |
|---|---|
initial blocks | The primary structural element — each is a concurrent testbench thread; runs in Reactive |
final blocks | Run at simulation end — ideal for summary, coverage report, final scoreboard checks |
| Tasks and functions | Standard building blocks of stimulus logic |
| Classes and objects | Full OOP support — class declarations, new(), method calls; the program is the entry point for class-based stimulus |
Variable types (logic, int, bit, byte, string, etc.) | All variable types are legal |
$exit, $finish, $display, $monitor, $strobe | Standard system tasks |
3.2 Illegal inside program
| Construct | Why it's forbidden |
|---|---|
always, always_ff, always_comb, always_latch | Continuously-running blocks are RTL constructs. Programs terminate when all initial blocks exit; always has no termination semantics |
| Module / UDP instantiation | Module hierarchy lives in the structural module domain; the program is a consumer, not a parent of hierarchy |
Net declarations (wire, tri, tri0, tri1, wand, wor, trireg, …) | Nets resolve multi-driver conflicts via strength-based resolution; programs only support variables |
specify blocks | Path delays and timing checks are gate-level constructs |
Analog / mixed-signal constructs (real nets, electrical, …) | AMS is outside the program block's scope |
generate blocks | Elaboration-time structural generation is a module-level construct |
The single-sentence summary: everything that runs continuously, instantiates structure, or models hardware behaviour is forbidden in program. Only stimulus-control code (initial / final / tasks / classes / variables) is allowed.
4. Syntax & Semantics — The Modern Module + Clocking Block Pattern
The canonical replacement for program is a top-level module that hosts the clock, instantiates the DUT and the interface, and uses an initial block (with clocking-block access) for the test stimulus. This is the pattern every UVM testbench uses.
// ── Interface with clocking block ─────────────────────────────
interface apb_if (input logic pclk);
logic presetn, psel, penable, pwrite, pready;
logic [31:0] paddr, pwdata, prdata;
clocking cb @(posedge pclk);
default input #1step output #1;
input pready, 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, prdata);
endinterface
// ── Testbench MODULE (not program) ────────────────────────────
module apb_tb;
logic pclk = 0;
always #5 pclk = ~pclk; // ← LEGAL in module (illegal in program)
apb_if bus (.pclk(pclk)); // ← LEGAL in module (illegal in program)
apb_slave dut (.bus(bus.DUT)); // ← LEGAL in module (illegal in program)
// ── Testbench logic in an initial block ───────────────────
initial begin
bus.presetn = 0;
repeat (4) @(bus.cb);
bus.presetn = 1;
@(bus.cb);
// Access signals through clocking block → race-free even in a module
bus.cb.psel <= 1; // ← drives with output skew
bus.cb.paddr <= 32'h100;
bus.cb.pwrite <= 1;
@(bus.cb);
bus.cb.penable <= 1;
@(bus.cb iff bus.cb.pready); // ← reads with input skew
bus.cb.psel <= 0;
bus.cb.penable <= 0;
$display("Write done at %0t", $time);
#100;
$finish;
end
endmoduleThree things this module does that a program cannot:
- Hosts the clock generator (
always #5 pclk = ~pclk) —alwaysis illegal inprogram. - Instantiates the interface and the DUT — module/interface instantiation is illegal in
program. - Lives in the same scope as the structural hierarchy — no separate "test" elaboration unit needed.
One thing this module does the same as a program: race-free DUT-signal access through bus.cb.signal and @(bus.cb). The clocking block provides the race elimination; the surrounding module provides the structural flexibility.
4.1 $exit vs $finish discipline
Even though modern testbenches typically use module (and therefore $finish), the two-keyword distinction matters when reading legacy program-based code:
$exit— graceful: ends this program block; simulation ends when every program block has exited (other programs'finalblocks fire normally).$finish— abrupt: halts the entire simulation immediately, including every program and module, regardless offinalblocks.
In a module, use $finish (there is no $exit semantic for modules). In a program, prefer $exit for graceful multi-program shutdown; reserve $finish for unrecoverable errors.
5. Simulation View — Same Reactive Semantics, Different Hosting
Both program initial and module initial begin @(cb); ... end produce the same scheduling behaviour for clocking-block accesses:
- The
@(cb)event control wakes in the Reactive region (post-NBA) regardless of whether the surrounding scope is aprogramor amodule. - The clocking-block output assignment (
cb.signal <= value) schedules the drive with the output skew applied; same NBA-region scheduling either way. - Signal reads through the clocking block (
x = cb.signal) get the input-skew-sampled value; same Postponed-region sample point either way.
The only scheduling-region difference: a program initial block runs entirely in the Reactive region, including code that touches signals outside the clocking block. A module initial block runs in the Active region for non-clocking-block code, and only the clocking-block accesses are Reactive-shifted.
For testbenches that always go through the clocking block (the modern UVM pattern), the difference is invisible. For testbenches that occasionally bypass the clocking block to read raw signals, the program block's blanket Reactive guarantee is the safety net the module pattern doesn't provide — but the right fix is to not bypass the clocking block, not to wrap in a program block.
6. Waveform — Not Applicable for This Lesson
This lesson is architectural, not timing-specific. The race-elimination waveform was shown in 15.1 program-block §6; the clocking-block timing model in 15.2 clocking-blocks-deep-dive §3; the skew-window waveform in 15.3 input-output-skews §3. The decision matrix in this lesson — when to use program vs module — is structural, captured in the §7 decision table and §8 verification view. Adding another waveform here would duplicate prior figures without teaching anything new. This section is intentionally omitted; the topic does not warrant it.
7. Synthesis — Not Applicable
program and module are both procedural simulation constructs. Synthesis tools accept module (it's the structural unit synthesised to gates) but reject program entirely; clocking blocks are sim-only regardless of the host. This section is intentionally omitted; the topic does not warrant it.
8. Verification View — Decision Matrix and Modern Patterns
8.1 The architectural decision table
Real testbench-construction decisions reduce to a small set of questions:
| Question | Answer leads to |
|---|---|
| Is this a UVM testbench? | module (UVM does not use program blocks; the test wrapper is always a module) |
Do you need always blocks (clock generator, monitor)? | module (only modules support always) |
| Do you need to instantiate other modules (DUT, sub-modules, interfaces)? | module (program cannot instantiate) |
| Building a new production testbench from scratch? | module + clocking block (industry standard, UVM-compatible) |
| Short directed test with stimulus only, no class-based agents? | either — both work |
| Are you accessing DUT signals directly without a clocking block? | program (the Reactive guarantee catches races a module would miss); or better: fix the architecture to go through a clocking block and use module |
| Maintaining an existing program-based codebase? | continue using program — no reason to refactor working code; understand the pattern and maintain consistently |
| Teaching / learning environments? | program is pedagogically valuable — illustrates the race condition and its solution cleanly |
The single highest-leverage rule: for any new production code, use module + clocking block. The clocking block provides the race-free guarantee; the module provides the structural flexibility for everything else (clocks, instantiation, scaling to UVM).
8.2 The UVM pattern — module-based by architectural necessity
UVM components — uvm_test, uvm_env, uvm_agent, uvm_driver, uvm_monitor, uvm_scoreboard, uvm_sequencer — are all classes. They are instantiated inside a module top level. The module hosts the clock, the DUT, the interfaces (with their clocking blocks); the UVM components access DUT signals through the interface's clocking block via a virtual interface (virtual apb_if vif).
module tb;
logic clk = 0;
always #5 clk = ~clk; // always — module only
apb_if bus (.pclk(clk)); // interface with clocking block
apb_slave dut (.bus(bus.DUT)); // DUT instantiation — module only
initial begin
uvm_config_db#(virtual apb_if)::set(null, "*", "vif", bus);
run_test(); // UVM phase machinery takes over
end
endmoduleUVM's uvm_driver::run_phase() runs inside the test's initial chain. The driver reads / drives via vif.cb.signal <= value and @(vif.cb). Every clocking-block access is race-free; the surrounding module provides the structural scaffolding. No program block anywhere.
8.3 Where program blocks are still appropriate
| Situation | Use program? | Reason |
|---|---|---|
| Non-UVM directed tests with simple stimulus + no class agents | Yes — clean choice | Short test; automatic termination on last initial exit is convenient |
| UVM-based testbench | No | UVM architecture uses modules; mixing program and UVM causes confusion |
| Testbench accessing signals directly (not through clocking block) | Yes — extra safety | Reactive-region guarantee catches races a module would miss; but better to fix the architecture |
| Teaching / learning environments | Yes — pedagogically valuable | Illustrates the race problem and its Reactive-region solution clearly |
| Existing codebase that uses program blocks | Continue using | No reason to refactor working code; maintain the pattern consistently |
| New production testbench from scratch | No — use module + clocking block | Industry standard, UVM-compatible, no restrictions on always blocks or module instantiation |
The honest summary: in a 2026 production verification environment, program blocks are a teaching tool and a legacy-codebase pattern, not a default architectural choice.
9. Industry Usage — Where the Decision Lands in Real Verification
- UVM (every shop, every chip) — module-based top level, interface with clocking block, virtual interfaces inside agents, no program blocks anywhere. The convention is universal across Synopsys VC, Cadence vManager, Siemens Questa, Aldec Riviera-PRO regression flows.
- Pre-UVM verification stacks (Vera, e, OpenVera) — these languages had separate test scheduling regions; SystemVerilog
programwas the standardisation of that concept. Legacy V2K + Vera codebases ported to SystemVerilog often used program blocks as the migration path; many of those are still in maintenance today. - Reference designs and academic course material — program blocks appear frequently because they make the race-condition teaching concrete. The teaching is correct; the production pattern moved on.
- Vendor VIP libraries (Synopsys VC, Cadence VIP Catalog, Siemens VIP) — all module-based, all interface + clocking block. Modern VIP consumed via UVM agents; no program blocks in the consumer or the VIP itself.
- Open-source verification frameworks (cocotb, UVM-Python wrappers, Verilator harnesses) — cocotb is Python-driven so doesn't use SV testbench primitives at all; UVM-Python wrappers expose the module + clocking-block pattern through Python bindings.
- Formal verification tools (JasperGold, OneSpin, Conformal) — properties live in interfaces or modules; program blocks are typically out of scope for formal flows entirely.
- Tool flow / regression infrastructure — the test-runner harnesses, log parsers, coverage mergers all expect module-based testbench top files. Program-based tops are second-class citizens in most regression frameworks (work, but with edge cases around
$exitsemantics and multi-test simulation invocation).
10. Design Review Notes — What a Senior Will Flag
| Pattern in the diff | What review will say |
|---|---|
New UVM testbench using program test; for the test wrapper | "UVM convention is module wrapper. program is legacy / pre-UVM. Refactor to module tb; ... run_test(); endmodule. The class-based UVM components don't need program-block hosting." |
program test; always #5 clk = ~clk; endprogram | "Compile error — always forbidden in program. Move the clock generator to the surrounding module; pass clk into the program through the port list. Or — better — convert the entire test to a module pattern." |
program test; my_dut dut(...); endprogram | "Cannot instantiate modules inside program. Instantiate the DUT in the top-level module; pass it to the program via an interface. Or convert to module." |
program test; ... $finish; endprogram | "Use $exit for graceful program termination inside a program block. $finish is the nuclear option that kills every other program / module immediately." |
| Module testbench accessing DUT signals directly without clocking block | "Race risk — module initial runs in Active region, before NBA. Use a clocking block in the interface and access via @(bus.cb); bus.cb.signal <= value;. The race fix is the clocking block, not switching to program." |
| Half-program / half-module testbench architecture | "Half-fixes don't work. The non-program parts still race. Either commit to program-based (rare for new code) or module-based (the standard) — and apply the clocking-block discipline uniformly across the chosen architecture." |
| Program block in a regression harness that runs multiple tests per simulation | "$exit semantics interact poorly with multi-test regression. Module-based tests with $finish after each test work cleanly with every harness; programs don't. Migrate." |
wire data_in = some_logic; inside a program | "Net declarations illegal in program. Use logic data_in = some_logic; instead. If you need a true net (multi-driver resolution), move that logic to a module." |
generate ... endgenerate inside program | "Forbidden. generate is elaboration-time structural; programs don't elaborate that way. Move to the surrounding module." |
| Comment "// using program for race safety" on a testbench that goes entirely through a clocking block | "Redundant — the clocking block already provides race safety. The program block's extra Reactive guarantee adds nothing here. Convert to module for the structural flexibility and remove the comment." |
The single highest-value rule: for new production code, default to module + clocking block. The architectural and tool-flow benefits dominate the theoretical Reactive guarantee that program adds.
11. Debugging Guide — Real Failures, Real Fixes
UVM test fails to elaborate; 'always' rejected by compiler
ALWAYS-IN-PROGRAMprogram uvm_test_wrapper;
logic clk = 0;
always #5 clk = ~clk; // ERROR: always forbidden in program
initial run_test();
endprogram"always block not allowed inside program". UVM test never starts. New engineer reading UVM examples online tried to wrap the test in a program block.module tb top. Trying to wrap UVM in a program both fails the language restriction (always for the clock) AND violates UVM convention.module tb; logic clk = 0; always #5 clk = ~clk; apb_if bus(.pclk(clk)); dut u_dut(.bus(bus.DUT)); initial begin uvm_config_db#(virtual apb_if)::set(null, "*", "vif", bus); run_test(); end endmodule. UVM expects this pattern; tooling expects this pattern; reviewers expect this pattern.Module testbench reads pre-NBA q despite the comment 'using clocking block'
DIRECT-SIGNAL-ACCESS-IN-MODULEmodule tb;
apb_if bus(.pclk(clk));
initial begin
@(posedge clk); // BUG: raw clock, not bus.cb
$display("%h", bus.prdata); // reads prdata in Active region
end
endmodule@(posedge clk) and accesses bus.prdata directly — bypassing the clocking-block sampling.bus.prdata (without bus.cb. prefix) reads the signal in the Active region — pre-NBA. The race comes back.@(bus.cb); x = bus.cb.prdata;. Convert every direct signal access in the test to clocking-block access. The discipline rule for module-based testbenches is "every cycle-accurate DUT signal touch goes through the clocking block — no exceptions."Other programs' final blocks never run; coverage report missing data
$FINISH-INSTEAD-OF-$EXITprogram test_a;
initial begin
run_directed_test();
$finish; // BUG: kills test_b and others
end
endprogram
program test_b;
initial run_random_test();
final $display("[test_b] coverage = %0d%%", get_coverage());
endprogramtest_a reports complete. test_b's coverage summary never prints. Regression log is missing the per-test coverage data. Engineer assumes test_b crashed.$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.$exit instead of $finish inside program blocks. $exit ends only this program; the simulator continues running other programs (so their final blocks fire). Simulation ends naturally when every program exits. Reserve $finish for unrecoverable errors. Better long-term: convert to module-based testbenches with single-test regression invocation.`generate` block compile-rejects when wrapped in program
GENERATE-IN-PROGRAMprogram test;
parameter N = 4;
generate
for (genvar i = 0; i < N; i++)
initial $display("agent %0d", i);
endgenerate
endprogram"generate not allowed inside program block". Engineer expected generate to work everywhere it works in modules.generate is an elaboration-time structural construct. Program blocks are not elaborated the same way as modules; they're test-runtime units. The language forbids generate in program to keep the elaboration model clean.for loop inside an initial block (for (int i = 0; i < N; i++) begin $display("agent %0d", i); end), or move the structural generation into the surrounding module. For the N-agent spawn pattern, use a runtime loop with automatic capture and fork ... join_none — exactly the pattern from 14.3 fork-join_none.Mixed module / program testbench: race still bites where it wasn't expected
HALF-PROGRAM-HALF-MODULEmodule tb_top;
logic clk = 0;
always #5 clk = ~clk;
apb_if bus(.pclk(clk));
program inner_test (apb_if.TB bus);
initial begin
@(bus.cb);
bus.cb.psel <= 1; // race-free (clocking block)
end
endprogram
inner_test test (.bus(bus.TB));
// ── BUG: this assertion is in the MODULE, not in the program
always_ff @(posedge clk) begin
if (bus.psel && bus.pready) // reads bus.psel in Active region
assert(bus.prdata !== 'x);
end
endmodulepsel cleanly via the clocking block. The module's always_ff assertion sees inconsistent psel values — sometimes pre-edge, sometimes post-edge — and intermittently fires false assertion violations. Cross-simulator instability.always_ff direct signal reads are not (Active-region sampling). Half-fixes leave half the race.default clocking bus.cb; property p; psel && pready |-> prdata !== 'x; endproperty); this gives the assertion the same Reactive-region sampling as the driver. (2) Refactor to all-module architecture, keep everything race-free via clocking-block access. Don't mix program and module testbench layers; the discipline must be uniform.12. Interview Insights — What Interviewers Actually Probe
Forbidden: always / always_ff / always_comb / always_latch (continuously-running blocks belong in modules); module / UDP instantiation (program is a consumer, not a hierarchy parent); net declarations like wire / tri (programs only support variables); specify blocks (gate-level constructs); analog / mixed-signal constructs; generate blocks (elaboration-time structural). Allowed: initial, final, tasks, functions, classes, variable types, system tasks like $exit / $finish / $display.
The restrictions enforce the architectural separation between hardware-modelling constructs (Active / NBA scheduler regions) and testbench-control constructs (Reactive region). They're the price of the program block's race-free guarantee.
13. Exercises
1. Design — modern testbench skeleton (Foundation)
Write a module tb; that instantiates an APB DUT and an APB interface with a clocking block. In an initial block, perform a single APB write transaction (psel/penable/pwrite/paddr/pwdata setup → wait for pready → deassert). Use the clocking-block discipline throughout (no raw signal access). Confirm no program block is needed.
2. Debug — the half-fixed race (Intermediate)
A teammate's testbench wraps its driver in a program block (which uses the clocking block correctly) but the assertions are declared in a surrounding module using always_ff @(posedge clk). The assertions intermittently fire false violations. Identify the bug class and write the canonical fix.
3. Code review — the over-restricted program (Intermediate)
A teammate submits a program test; logic clk = 0; always #5 clk = ~clk; my_dut dut(...); initial begin ... $finish; end endprogram and asks why their code won't compile. Identify all four architectural violations and write the canonical refactor (convert to module + clocking block).
4. Trade-off — program vs module + clocking block (Advanced)
A junior teammate argues: "always use program — it gives the Reactive guarantee for free, even if you accidentally bypass the clocking block. Why would anyone not use program for safety?" Argue the case against the blanket rule from the architectural angle (UVM compatibility, regression tooling, always blocks, module instantiation). Construct a scenario where a UVM testbench wrapped in program cannot work, and explain why.
14. Summary — Module 15 Closer
program solves the RTL-testbench race by running entirely in the Reactive region — but at the cost of forbidding always, module instantiation, and net declarations. The industry has largely moved on: UVM uses module + clocking block, and the clocking block alone provides the race-free guarantee. The program block was always a wrapper that forced the clocking-block discipline; the discipline matters, the wrapper does not.
Defaults to memorise. New production code: module + clocking block + interface. UVM testbenches: always module-based, no program blocks anywhere. Use $exit in programs, $finish in modules. The clocking block is the key insight — read with @(bus.cb), drive with bus.cb.signal <= value, never bypass it. Programs are appropriate for teaching, directed-test simplicity, and legacy maintenance — not new production architecture.
Module 15 complete. Four lessons that together cover the race-free testbench architecture from problem to production discipline:
- 15.1 Program Block vs Module — the Reactive-region scheduling that eliminates the TB-DUT race.
- 15.2 Clocking Blocks — Deep Dive, SVA, Coverage, Multi-Clock SoC, CDC — the per-domain timing primitive that anchors SVA, functional coverage, multi-clock-SoC verification, and CDC.
- 15.3 Input & Output Skews — the granular per-signal sampling and driving timing control.
- 15.4 Program Block Limitations & Best Practices (this lesson) — the operational discipline: when to use program vs module, why UVM moved on, the modern
module + clocking blockstandard.
The architectural arc: §1 introduces the race; §2 shows how the same fix scales to multi-clock SoCs and becomes the timing foundation for SVA / coverage / CDC; §3 gives the per-signal control; §4 closes the loop with the production-architecture decision. The whole module is one composed argument: the clocking block is the load-bearing construct; the program block was a wrapper that's no longer the production default.
Next module: 16 — Packages (begins with 16.1 Package Declaration & Usage) — how to share types, parameters, functions, and tasks across modules, interfaces, and programs without repeating declarations everywhere.