Part 2 · OOP for Verification · Intermediate

Constants & Enums in Classes

const class properties (instance vs static const), enum-typed transaction fields, and lookup table idioms.

const class properties: two flavors

A const class property is write-once. SystemVerilog gives you two distinct flavors with different semantics. A static const is a true class-level constant: initialized at its declaration, identical for every object, no per-object storage. An instance const (non-static) may instead be assigned once in the constructor — so each object can carry a different value that is then frozen for that object's lifetime. That second flavor is the interesting one: it models 'fixed at birth' attributes like a port number or an ID.

systemverilog
class bus_txn;
  // Class-level constant: one value, known at elaboration
  static const int MAX_BURST = 16;

  // Instance constant: set once in new(), then frozen per object
  const int port_id;

  rand bit [31:0] addr;

  function new(int port);
    port_id = port;        // legal: the ONE allowed assignment
  endfunction

  function void set_port(int x);
    // port_id = x;        // COMPILE ERROR: const after construction
  endfunction

  constraint c_burst { addr[3:0] < MAX_BURST; }
endclass

bus_txn t0 = new(0);   // t0.port_id == 0 forever
bus_txn t1 = new(1);   // t1.port_id == 1 forever

Why const instead of a plain field

  • The compiler enforces immutability — no method, subclass, or stray test code can mutate it.

  • Readers know the value cannot change, which makes reasoning about long sequences far easier.

  • Named constants (MAX_BURST) replace magic numbers in constraints and checks with one authoritative definition.


Enum-typed fields for transaction kinds

Transaction classes almost always carry a 'kind' field — read vs write, request vs response. Encoding it as bit [1:0] invites bugs: nothing stops an assignment of 3 when only 0–2 are meaningful. An enum field gives named values, compile-time rejection of bare-integer assignment, automatic name() for logs, and clean randomization — the solver picks only legal enum values. Define the enum in a package (not inside the class) so monitors, drivers, and scoreboards all share one definition.

systemverilog
package bus_pkg;
  typedef enum bit [1:0] { READ, WRITE, IDLE } kind_e;
endpackage

class bus_txn;
  import bus_pkg::*;
  rand kind_e     kind;
  rand bit [31:0] addr;

  // Solver only ever picks READ/WRITE/IDLE; weight them by name:
  constraint c_mostly_writes {
    kind dist { WRITE := 6, READ := 3, IDLE := 1 };
  }

  function void print();
    // .name() — free, readable logging
    $display("txn kind=%s addr=%0h", kind.name(), addr);
  endfunction
endclass
diagram
RAW BITS vs ENUM FIELD

  bit [1:0] kind;                 kind_e kind;
  ──────────────                  ─────────────
  kind = 3;     // silent bug     kind = 3;        // COMPILE ERROR
  kind = 1;     // what is 1?     kind = WRITE;    // self-documenting
  $display(kind);  // "1"         $display(kind.name());  // "WRITE"
  randomize  0..3 all legal      randomize  named values only

Lookup table idioms

Constants and enums combine into a powerful idiom: a static lookup table indexed by enum . Instead of a case statement repeated in every method that needs per-kind data (latency, expected response, legal length), store the mapping once as a static associative array keyed by the enum. One table, one place to update when the protocol changes, and every object shares it at zero per-object cost.

systemverilog
class bus_txn;
  import bus_pkg::*;
  rand kind_e kind;

  // One shared table: enum → cycles of expected latency
  static const int latency_tbl[kind_e] = '{
    READ  : 4,
    WRITE : 2,
    IDLE  : 1
  };

  function int expected_latency();
    return latency_tbl[kind];   // replaces a case statement
  endfunction
endclass

Interview angle: 'where would you put protocol timing constants shared by driver and monitor?' The strong answer is a static const table (or package-level parameters) referenced by both — never duplicated literals, and never instance fields, since the values are class-level facts, not per-object state.

Key takeaways

  • static const = one class-wide constant; instance const = per-object, set exactly once in new().

  • Enum-typed kind fields give compile-time safety, solver-friendly randomization, and name() logging.

  • Define shared enums in a package so every component agrees on one definition.

  • Static const lookup tables indexed by enum replace scattered case statements with one source of truth.

Common pitfalls

  • Assigning an instance const anywhere except the constructor — compile error.

  • Initializing a non-static const at the declaration when you needed per-object values — it freezes one value for all.

  • Declaring the enum inside one class — other components end up with duplicate, drifting definitions.

  • Using raw bit-vectors for kind fields — illegal encodings randomize in and pass silently.