Part 6 · Testbench Architecture · Intermediate

program Blocks: Use or Skip?

What program changes about scheduling, the race-avoidance theory, why much of industry skips it in favor of clocking blocks, and how to discuss it in interviews.

What program actually changes

A program block is a module-like container whose statements execute in the Reactive region of the SystemVerilog event scheduler — after all design (Active-region) activity for that time slot has settled. The intent: if testbench code runs strictly after design code, the TB always samples post-update values and a whole class of TB-vs-DUT races disappears by construction.

diagram
SIMPLIFIED EVENT REGIONS IN ONE TIME SLOT

  ┌────────────┐  design always/assign blocks evaluate
  │  Active    │  (module code lives here)
  ├────────────┤
  │  NBA       │  nonblocking assignment updates
  ├────────────┤
  │  Observed  │  property/assertion evaluation
  ├────────────┤
  │  Reactive  │  program-block code runs HERE
  │            │  (testbench sees settled design state)
  ├────────────┤
  │  Re-NBA    │  NBA updates from reactive code
  └────────────┘
       │ (loop until quiescent, then advance time)

  Theory: TB in Reactive region ⇒ TB can never race the design.

Side effects of program

  • Blocking assignments to design signals from a program are scheduled into the Re-NBA region — write races also avoided.

  • When every initial block inside all programs completes, simulation ends implicitly (like an automatic $finish).

  • program blocks cannot contain always blocks; they are stimulus containers, not design containers.


The case for skipping it

Most modern flows — including UVM itself — do not use program blocks. The race problem program solves is solved more locally and more completely by clocking blocks : input sampling at #1step (just before the edge) and output driving with a specified skew (just after the edge) eliminate sample/drive races at the signal boundary, regardless of which region the calling code runs in.

systemverilog
// Approach A — program block (Spear-era style)
program automatic test (bus_if bif);
  initial begin
    base_test t = new();
    t.vif = bif;
    t.run();              // runs in Reactive region
  end
endprogram                 // sim ends when this initial completes

// Approach B — plain module + clocking blocks (industry-common)
module tb_top;
  // ... clock, reset, interface bif, DUT ...
  initial begin
    base_test t = new();
    t.vif = bif;
    t.run();
  end
  // Race safety comes from the interface clocking blocks:
  //   @(vif.drv_cb)  vif.drv_cb.valid <= 1;   // skewed drive
  //   data = vif.mon_cb.rdata;                // #1step sample
endmodule

Why industry largely moved on

  • Clocking blocks fix races at the pin boundary, which is where they occur — program fixes them globally with scheduling semantics few engineers can reason about precisely.

  • The implicit end-of-simulation when program initials finish surprises people and fights explicit end-of-test orchestration.

  • UVM runs entirely in modules; mixing program-region semantics with UVM phasing created confusion for no benefit.

  • Tool support and team familiarity: a plain module behaves identically everywhere; program corner cases historically varied.


How to frame it in an interview

This is a judgment question disguised as a syntax question. The strong answer covers both sides and lands on practice: program exists to schedule TB code into the Reactive region and kill TB/design races by construction; in practice, disciplined use of clocking blocks for all pin access achieves the same race freedom with simpler semantics, which is why UVM-era testbenches are module-based. Knowing why program exists matters more than using it.

If you do use program

  • Declare it automatic so tasks/functions get per-call stacks under concurrency.

  • Still use clocking blocks — program does not specify drive skew at the pin.

  • Remember the implicit $finish; add explicit control if the env owns end-of-test.

Key takeaways

  • program schedules TB code in the Reactive region so it always sees settled design values.

  • Clocking blocks solve the same race problem at the pin boundary, which is where races actually happen.

  • UVM and most modern flows use plain modules + clocking blocks; program is legal but increasingly rare.

  • Interview answer: explain the scheduling theory, then the practical reason it is commonly skipped.

Common pitfalls

  • Relying on program for race safety while bypassing clocking blocks — drive skew at pins is still unspecified.

  • Being bitten by the implicit end-of-sim when the program's initial finishes before drain time.

  • Claiming “program is required for testbenches” in an interview — instantly dates your knowledge.

  • Mixing program Reactive semantics with module-based components and then debugging “impossible” orderings.