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.
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 foreverWhy 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.
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
endclassRAW 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 onlyLookup 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.
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
endclassInterview 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.