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.

diagram
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 variable

The 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.

systemverilog
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
endmodule

Accessing 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.

systemverilog
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.