Part 2 · OOP for Verification · Intermediate
Static Properties & Methods
Class-level state shared across objects, transaction ID counters, static method restrictions, and static lifetime.
One copy per class, not per object
A normal (instance) property gets a fresh copy in every object created with new(). A static property gets exactly one copy that belongs to the class itself — every object sees and modifies the same storage. Semantically, a static property exists from the start of simulation, even before any object of the class is constructed, and it lives until simulation ends. It is class-level global state with the class name as its namespace.
This is why statics are the standard idiom for cross-object bookkeeping : how many transactions were ever created, what is the next unique ID, how many objects of this type are currently alive. No single object can own that information, because it describes the whole population of objects.
STATIC vs INSTANCE LIFETIME
time 0 ──────────────────────────────────────────► sim end
│
│ static int my_txn::count ◄── exists the whole time,
│ ════════════════════════════ ONE copy, no new() needed
│
│ t1 = new() t1 dropped (garbage collected)
│ ├─ t1.id ──────────┤ ◄── instance property:
│ born at new(), dies
│ t2 = new() with the object
│ ├─ t2.id ────────────►
│
│ t1.count, t2.count, my_txn::count → all the SAME variableThe transaction ID counter idiom
The classic use: stamp every transaction with a unique ID at construction time, by incrementing a shared static counter inside new(). The ID makes log messages and scoreboard mismatches traceable to one specific object.
class bus_txn;
static int count = 0; // one copy for the whole class
int id; // one copy per object
rand bit [31:0] addr;
rand bit [31:0] data;
function new();
id = count; // stamp unique ID
count++; // shared counter advances
endfunction
function void print();
$display("txn[%0d] addr=%0h data=%0h", id, addr, data);
endfunction
endclass
module top;
initial begin
bus_txn a = new(); // a.id = 0, count = 1
bus_txn b = new(); // b.id = 1, count = 2
bus_txn c = new(); // c.id = 2, count = 3
$display("created %0d txns", bus_txn::count); // class-scope access
end
endmoduleAccessing statics
bus_txn::count — class-scope operator; works with no object at all and reads as 'class-level' to reviewers. Preferred.
a.count — legal through any handle, but misleading: it looks like instance state when it is not.
Static properties may be initialized at declaration (static int count = 0;) — this runs once, not per object.
Static methods and their restrictions
A static method belongs to the class, so it can be called without any object: bus_txn::report(). The price: a static method has no this handle, because there is no object behind the call. It therefore cannot read or write non-static properties, cannot call non-static methods, and cannot be virtual — virtual dispatch needs an object whose type decides which override runs, and a static call has no object.
class bus_txn;
static int count = 0;
int id;
// Legal: touches only static state
static function int how_many();
return count;
endfunction
// ILLEGAL examples (compile errors):
// static function int get_id();
// return id; // no 'this' → no instance 'id' to read
// endfunction
// static virtual function void f(); // static cannot be virtual
endclass
initial $display("%0d txns so far", bus_txn::how_many());When static hurts: parallel environments
Static state is shared across the entire simulation , not per testbench instance. If your environment is instantiated twice — two agents, multi-core DUT, or a layered env reusing a sub-env — every instance shares the same static variables. A static 'expected response queue' or static configuration knob silently couples environments that should be independent. The ID counter is safe because IDs only need to be unique globally; a static scoreboard queue is a bug. Interviewers probe exactly this: 'what breaks if you make the scoreboard queue static?'
Key takeaways
Static properties: one copy per class, alive for the whole simulation, shared by all objects.
Use class-scope access (class_name::member) to make class-level intent obvious.
Static methods have no this — they can only touch static members and cannot be virtual.
Statics couple every instance of an environment — fine for unique IDs, wrong for per-env state.
Common pitfalls
Making per-environment state (queues, config) static — multiple env instances corrupt each other.
Trying to read instance properties from a static method — no this handle exists.
Declaring a static method virtual — illegal; dispatch requires an object.
Forgetting statics persist across tests in multi-test flows — stale counts leak into the next test.