Part 2 · OOP for Verification · Intermediate

Parameterized Classes

Value and type parameters, specializations as distinct types, the static-per-specialization trap, and parameterized scoreboards.

Value and type parameters

A parameterized class is a template : one source definition that the compiler stamps out into concrete classes when you supply parameters. Parameters come in two kinds — value parameters (#(int WIDTH = 8)) that set sizes and constants, and type parameters (#(type T = int)) that let one container class hold any payload type. This is how SystemVerilog gets generic containers without dynamic typing: a fifo#(bus_txn) and a fifo#(pkt) share source code but are fully type-checked against their own payload.

systemverilog
class fifo #(type T = int, int DEPTH = 8);
  local T q[$];

  function bit try_put(T item);
    if (q.size() >= DEPTH) return 0;
    q.push_back(item);
    return 1;
  endfunction

  function bit try_get(ref T item);
    if (q.size() == 0) return 0;
    item = q.pop_front();
    return 1;
  endfunction

  function int level();
    return q.size();
  endfunction
endclass

// Specializations — supply parameters at use site
fifo #(bus_txn, 16) txn_fifo  = new();
fifo #(byte)        byte_fifo = new();   // DEPTH defaults to 8

Specializations are distinct types

This is the semantic point most engineers learn the hard way: fifo#(byte) and fifo#(int) are unrelated types . You cannot assign a handle of one to the other, they do not share a base class (unless you give them one explicitly), and — the classic trap — each specialization gets its own copy of every static member . A static counter in a parameterized class does not count across all specializations; it counts per specialization.

diagram
STATIC-PER-SPECIALIZATION TRAP

  class item #(int W = 8);
    static int count = 0;
  endclass

           one source template
                   │
        ┌──────────┼──────────┐
        ▼          ▼          ▼
   item#(8)    item#(16)   item#(32)     ◄── three DISTINCT classes
        │          │          │
   count = 5   count = 2   count = 0     ◄── three SEPARATE statics!

  item#(8)::count   5
  item#(16)::count  2      no single "total across widths" exists
  item#(8) h8; item#(16) h16;
  h8 = h16;                  ◄── COMPILE ERROR: unrelated types

typedef your specializations

Because the full specialization name is verbose and must match exactly everywhere (including default parameters — fifo#(byte) and fifo#(byte, 8) are the same type, but it is easy to mistype one site), the standard practice is one typedef per specialization in a shared package. Every declaration then uses the typedef, so a width change is a one-line edit.

systemverilog
package tb_types_pkg;
  typedef fifo #(bus_txn, 16) txn_fifo_t;
  typedef fifo #(byte,    64) byte_fifo_t;
endpackage

// Everywhere else:
import tb_types_pkg::*;
txn_fifo_t req_q  = new();
txn_fifo_t resp_q = new();   // guaranteed the same type as req_q

A parameterized scoreboard

The payoff: one scoreboard implementation reused for every transaction type in the testbench. The only requirement is a contract on T — here, that it provides compare(). This is the same idea behind the built-in mailbox #(T), which is itself a parameterized class: mailbox #(bus_txn) gives compile-time type checking on put/get instead of the type-unsafe unparameterized mailbox. Interviews often ask exactly that comparison: why parameterize a mailbox? Answer: a typed mailbox rejects wrong-type puts at compile time.

systemverilog
class scoreboard #(type T = bus_txn);
  local T   expected_q[$];
  local int mismatches;

  function void add_expected(T t);
    expected_q.push_back(t);
  endfunction

  function void check(T actual);
    T exp;
    if (expected_q.size() == 0) begin
      $error("unexpected item");
      mismatches++;
      return;
    end
    exp = expected_q.pop_front();
    if (!exp.compare(actual)) begin   // contract: T has compare()
      mismatches++;
      $error("scoreboard mismatch");
    end
  endfunction
endclass

// Reused across protocols, each fully type-checked:
scoreboard #(bus_txn) bus_sb = new();
scoreboard #(eth_pkt) eth_sb = new();

Key takeaways

  • Value parameters set sizes; type parameters make generic, type-checked containers.

  • Each specialization is a distinct, unrelated type — handles do not cross-assign.

  • Every specialization owns its own static members — no counter spans specializations.

  • typedef each specialization once in a package so all use sites agree by construction.

Common pitfalls

  • Expecting one static counter across all specializations — each gets its own copy.

  • Assigning between different specializations' handles — compile error, unrelated types.

  • Spelling the specialization slightly differently at two sites — two distinct types, confusing errors.

  • Calling a method on T that some payload type lacks — the contract on T is implicit; document it.