Skip to content

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 always blocks (no always, always_ff, always_comb, always_latch). A program is meant to terminate; always runs 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 / tri net declarations. Programs are pure stimulus + check; net resolution belongs to the hardware-modelling side.
  • No specify blocks, no analog/mixed-signal, no generate blocks. 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 from clocking. A clocking block declared in an interface and accessed via @(cb) and <= samples in the Observed / Reactive regions whether the calling code is inside a program block or inside a module's initial block. 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 endmodule is exactly as race-free as the same code wrapped in a program. The clocking block is what creates the timing contract; the surrounding module vs program is a structural choice.
  • module enables what program forbids. 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. program cannot.
  • Architectural cost beats theoretical safety. The "extra Reactive guarantee" program provides 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.

The structural restrictions are the architectural argument. The two tables below summarise what the language allows and forbids inside a program block.

ConstructWhy it's allowed
initial blocksThe primary structural element — each is a concurrent testbench thread; runs in Reactive
final blocksRun at simulation end — ideal for summary, coverage report, final scoreboard checks
Tasks and functionsStandard building blocks of stimulus logic
Classes and objectsFull 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, $strobeStandard system tasks

3.2 Illegal inside program

ConstructWhy it's forbidden
always, always_ff, always_comb, always_latchContinuously-running blocks are RTL constructs. Programs terminate when all initial blocks exit; always has no termination semantics
Module / UDP instantiationModule 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 blocksPath delays and timing checks are gate-level constructs
Analog / mixed-signal constructs (real nets, electrical, …)AMS is outside the program block's scope
generate blocksElaboration-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.

SystemVerilog — modern module-based testbench with clocking block (no program needed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Three things this module does that a program cannot:

  1. Hosts the clock generator (always #5 pclk = ~pclk) — always is illegal in program.
  2. Instantiates the interface and the DUT — module/interface instantiation is illegal in program.
  3. 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' final blocks fire normally).
  • $finish — abrupt: halts the entire simulation immediately, including every program and module, regardless of final blocks.

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 a program or a module.
  • 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:

QuestionAnswer 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).

SystemVerilog — UVM-style top: module + interface + clocking block, no program
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

UVM'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

SituationUse program?Reason
Non-UVM directed tests with simple stimulus + no class agentsYes — clean choiceShort test; automatic termination on last initial exit is convenient
UVM-based testbenchNoUVM architecture uses modules; mixing program and UVM causes confusion
Testbench accessing signals directly (not through clocking block)Yes — extra safetyReactive-region guarantee catches races a module would miss; but better to fix the architecture
Teaching / learning environmentsYes — pedagogically valuableIllustrates the race problem and its Reactive-region solution clearly
Existing codebase that uses program blocksContinue usingNo reason to refactor working code; maintain the pattern consistently
New production testbench from scratchNo — use module + clocking blockIndustry 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 program was 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 $exit semantics and multi-test simulation invocation).

10. Design Review Notes — What a Senior Will Flag

Pattern in the diffWhat 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

1

UVM test fails to elaborate; 'always' rejected by compiler

ALWAYS-IN-PROGRAM
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
program uvm_test_wrapper;
    logic clk = 0;
    always #5 clk = ~clk;        // ERROR: always forbidden in program
    initial run_test();
endprogram
Symptom
Compilation fails with "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.
Root Cause
UVM is architecturally module-based — every UVM example uses a module tb top. Trying to wrap UVM in a program both fails the language restriction (always for the clock) AND violates UVM convention.
Fix
Convert the wrapper to a module: 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.
2

Module testbench reads pre-NBA q despite the comment 'using clocking block'

DIRECT-SIGNAL-ACCESS-IN-MODULE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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
Symptom
Module testbench shows the same TB-DUT race as if no clocking block existed. The interface has a clocking block, but the test code uses raw @(posedge clk) and accesses bus.prdata directly — bypassing the clocking-block sampling.
Root Cause
The clocking block provides race-free sampling only when you access signals through it. Raw bus.prdata (without bus.cb. prefix) reads the signal in the Active region — pre-NBA. The race comes back.
Fix
Always go through the clocking block: @(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."
3

Other programs' final blocks never run; coverage report missing data

$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 and others
    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.
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 (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.
4

`generate` block compile-rejects when wrapped in program

GENERATE-IN-PROGRAM
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
program test;
    parameter N = 4;
    generate
        for (genvar i = 0; i < N; i++)
            initial $display("agent %0d", i);
    endgenerate
endprogram
Symptom
Compile fails with "generate not allowed inside program block". Engineer expected generate to work everywhere it works in modules.
Root Cause
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.
Fix
Either use a runtime 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.
5

Mixed module / program testbench: race still bites where it wasn't expected

HALF-PROGRAM-HALF-MODULE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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
endmodule
Symptom
The program-block driver writes psel 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.
Root Cause
Mixing program and module testbench code partially fixes the race. The program's clocking-block driver writes are race-free; the module's always_ff direct signal reads are not (Active-region sampling). Half-fixes leave half the race.
Fix
Two clean options. (1) Move the assertion into the interface itself, using the clocking block (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:

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.