SystemVerilog · Module 9
Class Scope Resolution (::)
Reaching static members, package symbols, class-scoped enums, parameterised specialisations, and the UVM factory's type_id::create() idiom — names vs instances at compile time.
Module 9 · Page 9.16
What the :: Operator Does
In SystemVerilog, :: is the scope resolution operator. It tells the compiler: "look inside this specific namespace for the name that follows."
For classes, ClassName::member accesses something that belongs to the class itself — not to any particular object. You do not need a handle. You do not need new(). :: works directly on the class name.
dot (.) vs Scope Resolution (::)
. (dot) Needs a live object handle. Accesses instance members.
-
t.addr = 32'h0;
-
t.display();
-
t.txn_id
-
drv.cfg.clk_mhz :: (scope) No object needed. Accesses class-level members.
-
BusTxn::next_id
-
BusTxn::get_count()
-
ApbDriver::Config
-
AhbTxn::burst_e
class BusTxn;
static int next_id = 1;
static int total = 0;
rand bit [31:0] addr;
int txn_id;
function new(); txn_id = next_id++; total++; endfunction
static function int get_total(); return total; endfunction
endclass
module tb;
BusTxn t1, t2;
initial begin
// ── :: accesses class-level — no object needed ────────
$display("Before any new(): total = %0d", BusTxn::total); // 0
$display("Before any new(): next_id = %0d", BusTxn::next_id); // 1
$display("Static method: %0d", BusTxn::get_total()); // 0
t1 = new();
t2 = new();
// ── . accesses object-level — needs a live handle ─────
t1.addr = 32'h4000_0000;
$display("t1.addr = 0x%08h", t1.addr); // object member via dot
$display("t1.txn_id = %0d", t1.txn_id); // object member via dot
// ── :: still works after objects exist ───────────────
$display("After 2 new(): total = %0d", BusTxn::total); // 2
// ── Both forms reach the same static data ─────────────
$display("Via handle : %0d", t1.total); // 2 — also valid
$display("Via class : %0d", BusTxn::total); // 2 — preferred style
end
endmoduleFive Uses of :: in Classes
1. Static property
ClassName::static_prop Read or write a class-level variable without an object.
2. Static method
ClassName::method() Call a static function or task without creating an object.
3. Nested class type
Outer::Inner Access an inner class declared inside an outer class.
4. typedef / enum value
ClassName::ENUM_VAL Access an enum value or typedef declared inside a class.
5. Parameterised class
Class#(T)::member Access static members of a specific parameterised specialisation.
Each Use — With Code
Use 1 — Static Property
class Txn;
static int obj_count = 0;
static int next_uid = 1000;
int uid;
function new();
uid = next_uid++;
obj_count++;
endfunction
endclass
// Read static before creating any object
$display("obj_count = %0d", Txn::obj_count); // 0
Txn t1 = new();
Txn t2 = new();
// Read/write static via class name — preferred over t1.obj_count
$display("obj_count = %0d", Txn::obj_count); // 2
Txn::next_uid = 5000; // reset ID counter for a new test phaseUse 2 — Static Method
class ApbDriver;
static int verbosity = 0;
string name;
function new(string n); name = n; endfunction
static function void set_verbosity(int v);
verbosity = v;
$display("[ApbDriver] verbosity = %0d (affects ALL instances)", v);
endfunction
static function int get_verbosity();
return verbosity;
endfunction
endclass
// Call static method — no object at all
ApbDriver::set_verbosity(2);
ApbDriver d1 = new("drv0");
ApbDriver d2 = new("drv1");
// Both d1 and d2 now see verbosity=2
$display("verbosity = %0d", ApbDriver::get_verbosity()); // 2Use 3 — Nested Class Type
class SpiDriver;
class Config;
int clk_mhz = 10;
bit lsb_first = 0;
function void display();
$display("Config: clk=%0dMHz lsb=%0b", clk_mhz, lsb_first);
endfunction
endclass
Config cfg;
function new(); cfg = new(); endfunction
endclass
// Access the nested class from OUTSIDE using ::
SpiDriver::Config my_cfg = new();
my_cfg.clk_mhz = 50;
my_cfg.lsb_first = 1;
my_cfg.display(); // Config: clk=50MHz lsb=1Use 4 — typedef / enum Values Inside a Class
class AhbTxn;
typedef enum bit [2:0] {
SINGLE = 3'b000,
INCR = 3'b001,
WRAP4 = 3'b010,
INCR4 = 3'b011
} burst_e;
rand burst_e hburst;
rand bit [31:0] haddr;
endclass
// Use the enum TYPE from outside — via ::
AhbTxn::burst_e my_burst;
// Use individual enum VALUES — also via ::
my_burst = AhbTxn::INCR4;
$display("burst = %s (%03b)", my_burst.name(), my_burst); // INCR4 (011)
// In a constraint — compare against a class-scoped enum value
AhbTxn t = new();
void'(t.randomize() with { hburst == AhbTxn::WRAP4; });Use 5 — Parameterised Class Static Member
class Counter #(type T = int);
static int count = 0;
int id;
function new(); id = count++; endfunction
endclass
// Create objects of two different specialisations
Counter #(int) c1 = new();
Counter #(int) c2 = new();
Counter #(string) s1 = new();
// Each specialisation has its OWN static count
$display("int count = %0d", Counter#(int)::count); // 2
$display("string count = %0d", Counter#(string)::count); // 1
// Syntax: ClassName #(TypeArg) :: memberComplete Working Example — All Five Uses Together
// ════════════════════════════════════════════════════════════════
// Class with static members, nested class, and typedef enum
// ════════════════════════════════════════════════════════════════
class ApbTxn;
// ── typedef enum inside class ─────────────────────────────
typedef enum bit {
READ = 1'b0,
WRITE = 1'b1
} dir_e;
// ── Nested class inside ApbTxn ────────────────────────────
class Stats;
int pass_cnt = 0;
int fail_cnt = 0;
function void report();
$display("Stats: PASS=%0d FAIL=%0d", pass_cnt, fail_cnt);
endfunction
endclass
// ── Static members ────────────────────────────────────────
static int obj_count = 0;
static int next_id = 1;
static int verbosity = 0;
// ── Instance members ──────────────────────────────────────
rand bit [31:0] addr;
rand bit [31:0] data;
rand dir_e direction;
int txn_id;
constraint c_align { addr[1:0] == 2'b00; }
function new();
txn_id = next_id++;
obj_count++;
endfunction
// ── Static methods ────────────────────────────────────────
static function void set_verbosity(int v);
verbosity = v;
endfunction
static function int get_count();
return obj_count;
endfunction
// ── Instance method ───────────────────────────────────────
function void display();
if (verbosity >= 1)
$display("[APB#%0d] %s addr=0x%08h data=0x%08h",
txn_id,
(direction == WRITE) ? "WR" : "RD",
addr, data);
endfunction
endclass
// ════════════════════════════════════════════════════════════════
// Testbench demonstrating all five :: uses
// Note: :: type declarations must be at module scope, not inside
// an initial block — this is the correct SV syntax
// ════════════════════════════════════════════════════════════════
module tb;
// ── Declare :: types at module level (required in SV) ────
ApbTxn::dir_e forced_dir; // Use 4: enum type via ::
ApbTxn::Stats stats; // Use 3: nested class type via ::
ApbTxn txns[5]; // array of transaction handles
ApbTxn quiet; // single handle for silent test
initial begin
// ── Use 1: Static property — no object needed ─────────
$display("[1] Before new: obj_count = %0d",
ApbTxn::obj_count); // 0
// ── Use 2: Static method — no object needed ───────────
ApbTxn::set_verbosity(1); // enable display for all transactions
// ── Use 4: Assign enum value from inside class ────────
forced_dir = ApbTxn::WRITE;
$display("[4] Forced direction: %s", forced_dir.name());
// ── Use 3: Create a nested class object ───────────────
stats = new();
// Create 5 randomised transactions
foreach(txns[i]) begin
txns[i] = new();
void'(txns[i].randomize());
txns[i].display(); // instance method via dot
end
// ── Use 1 again: verify count via class name ──────────
$display("\n[1] After 5 new(): obj_count = %0d",
ApbTxn::get_count()); // 5
// ── Simulate pass/fail results in stats ───────────────
stats.pass_cnt = 4;
stats.fail_cnt = 1;
stats.report(); // Stats: PASS=4 FAIL=1
// ── Use 2: Turn off verbosity with static method ───────
ApbTxn::set_verbosity(0);
$display("\n[2] Verbosity now off — new transactions silent");
quiet = new();
void'(quiet.randomize());
quiet.display(); // prints nothing — verbosity=0
$display("[1] Final obj_count = %0d",
ApbTxn::obj_count); // 6
end
endmodule
// ════════════════════════════════════════════════════════════════
// EXPECTED OUTPUT:
// [1] Before new: obj_count = 0
// [4] Forced direction: WRITE
// [APB#1] WR addr=0x... data=0x...
// [APB#2] RD addr=0x... data=0x...
// [APB#3] WR addr=0x... data=0x...
// [APB#4] RD addr=0x... data=0x...
// [APB#5] WR addr=0x... data=0x...
//
// [1] After 5 new(): obj_count = 5
// Stats: PASS=4 FAIL=1
//
// [2] Verbosity now off — new transactions silent
// [1] Final obj_count = 6
// ════════════════════════════════════════════════════════════════Quick Reference
| Use | Syntax | Object needed? |
|---|---|---|
| Static property | ClassName::prop | No |
| Static method | ClassName::method(args) | No |
| Nested class | OuterClass::InnerClass obj = new(); | No (for type); Yes (for methods) |
| Enum type from inside class | ClassName::enum_type var; | No |
| Enum value from inside class | ClassName::ENUM_VALUE | No |
| Parameterised class static | ClassName#(T)::member | No |
| Instance property (dot) | handle.prop | Yes — must call new() first |
| Instance method (dot) | handle.method() | Yes — must call new() first |
Verification Usage — Where :: Shows Up in Real UVM Code
Open any UVM environment, grep for ::, and you will find it on every other line — accessing factory methods, enumerated types, package symbols, parameterised specialisations, and static helpers. Four recurring patterns dominate.
UVM factory — type_id::create()
Every UVM object constructed through the factory uses ::: apb_xact::type_id::create("tx", this). This is two nested scope resolutions — first into the class to reach its type_id handle, then into type_id to reach create(). The pattern is so pervasive that recognising it is the first sign a reader is comfortable with UVM.
<code>// Common UVM factory creation pattern
class my_test extends uvm_test;
task run_phase(uvm_phase phase);
apb_xact tx;
tx = apb_xact::type_id::create("tx"); // factory creation
assert(tx.randomize());
`uvm_info(get_type_name(), tx.convert2string(), UVM_MEDIUM)
endtask
endclass</code>Package-qualified types when wildcards collide
Production environments import multiple packages (uvm_pkg, the project's own packages, third-party VIP packages). When two packages export identically-named symbols — easy when the project defines its own uvm_sequence_item-derived base_xact while another VIP exports a different base_xact — qualification with :: is the only way to be specific.
Enum literals scoped to their enclosing class
A driver's internal state enum (typedef enum {S_IDLE, S_REQ, S_RESP, S_DONE} state_e;) is always referenced from outside the class as my_driver::S_IDLE. The qualified form documents ownership at the call site and prevents collisions with another component's identically-named enum.
Static helpers — counters, severity controllers, singletons
Every static utility method or static counter is accessed through ::: tb_logger::level = HIGH;, packet::next_id++;, env_config::get(). The convention reinforces the "one shared cell" semantics — readers immediately know they are touching class-wide state, not per-instance state.
Simulation Behavior — How the Compiler Resolves ::
Compile-time symbol-table lookup
Every :: is resolved by the compiler walking the symbol table: starting from the leftmost name (a class, package, or specialisation), descending into its scope, and continuing until every :: has been satisfied. The lookup is fully static — by the time simulation starts, every :: has been replaced by a direct address in the emitted code.
No runtime cost, no per-call dispatch
Unlike obj.method() on a virtual method (which loads the vtable, indexes into the slot, and indirect-calls), ClassName::method() compiles to a direct call. There is no vtable lookup because static methods do not participate in dispatch. The runtime is identical to a freestanding function call — fastest possible call path.
Resolution works on incomplete types in some cases
Because :: resolution happens at the symbol-table level, it can access certain features of forward-declared classes — particularly the type_id handle that UVM's ``uvm_object_utilsmacro registers. This is whymy_xact::type_id::create()often works even when only a forward declaration ofmy_xact` is in scope.
Wildcard imports do not create new qualified names
import pkg::*; makes pkg's public names directly visible without qualification, but it does not delete the qualified form — pkg::name still works. The two forms are equivalent for non-ambiguous names; they diverge when two imported packages export the same name, at which point the qualified form is the disambiguation tool.
<code>import uvm_pkg::*;
import my_pkg::*;
uvm_pkg::uvm_sequence_item req; // explicit
my_pkg ::uvm_sequence_item proj; // explicit
// Compiles unambiguously; reader
// knows exactly which version is
// being used at each declaration.</code><code>import uvm_pkg::*;
import my_pkg::*;
uvm_sequence_item req; // ERROR — which one?
// Both packages export
// 'uvm_sequence_item'.
// The compiler refuses to
// guess which package wins.</code>Waveform Analysis — Tracing ::-Resolved Calls in Debug
:: calls leave no special waveform footprint — they compile to ordinary direct calls. What is visible in debug is the name at the call site, which by convention includes the qualifying class or package. Log lines that include the fully-qualified name make it instantly clear which scope's symbol fired.
The trace pattern for static-helper calls
<code>function void scoreboard::log_event(string ev);
$display("[%0t] %s::log_event %s (tb_logger::level=%s)",
$time, $typename(this), ev, tb_logger::level.name());
endfunction
// Sample output:
// 100ns scoreboard::log_event txn-1 expected (tb_logger::level=MED)
// 100ns scoreboard::log_event txn-1 observed (tb_logger::level=MED)
// 150ns scoreboard::log_event level changed (tb_logger::level=HIGH)</code>ASCII view — qualified names make ownership self-documenting
<code>100ns scoreboard::log_event txn-1 expected (tb_logger::level=MED)
100ns scoreboard::log_event txn-1 observed (tb_logger::level=MED)
150ns apb_driver::reset_seq firing reset (tb_logger::level=MED)
200ns scoreboard::log_event level changed (tb_logger::level=HIGH)
↑ ↑
class scope shared static state
(where the call lives) (always qualified)</code>Industry Insights — Hard-Won Lessons About ::
Debugging Academy — Five Real Bugs Around the :: Operator
"Compile error: 'PI' is an unknown identifier"
DEBUGA test references a package-level constant PI and gets an "unknown identifier" error, even though the constant is clearly defined in the imported package.
<code>// math_pkg.sv
package math_pkg;
parameter real PI = 3.14159;
endpackage
// test.sv
// BUG: forgot to import or qualify
real circumference = 2.0 * PI * radius;</code>Without either an import math_pkg::*; (which makes PI visible unqualified) or a qualified reference math_pkg::PI, the symbol is unknown in the test scope.
Either (a) qualify the reference: real circumference = 2.0 * math_pkg::PI * radius; (clearer for one-off use), or (b) add import math_pkg::*; at the top of the test (cleaner for files that touch many symbols from the package).
"Wildcard import collision — compiler refuses to pick"
DEBUGTwo packages are imported with wildcards; the compiler errors out with "identifier 'uvm_sequence_item' is ambiguous — could refer to uvm_pkg::uvm_sequence_item or my_pkg::uvm_sequence_item."
<code>import uvm_pkg::*;
import my_pkg::*; // my_pkg also exports uvm_sequence_item
uvm_sequence_item req; // AMBIGUOUS — which one?</code>Two packages exporting the same symbol via wildcard imports create an unresolvable name conflict. The compiler refuses to silently guess.
Qualify the specific use: uvm_pkg::uvm_sequence_item req;. The qualification overrides the wildcard ambiguity at the use site. Alternatively, prefer explicit imports (import my_pkg::base_xact;) over wildcard imports — the latter scale poorly in large environments.
"Static counter accessed via . through a handle silently works but confuses reviewers"
DEBUGCode compiles and runs, but a code review flags obj.next_id with the comment "I thought next_id was supposed to be class-wide?"
<code>class packet;
static int next_id = 0;
int id;
function new();
id = next_id++;
endfunction
endclass
packet p = new();
$display("counter=%0d", p.next_id); // works, but misleading</code>SystemVerilog allows static members to be reached through an instance handle. It compiles silently, but it hides the "one shared cell" semantics — a reader naturally assumes p.next_id is per-instance.
Always access static members through the class name: $display("counter=%0d", packet::next_id);. The qualified form is the documentation that says "this is class-wide state." Make it a style-guide rule.
"handle::method() compile error — '::' requires a type name"
DEBUGA new OOP developer writes h::display() where h is a handle, expecting it to work like h.display(). Compile error: "'::' requires a class or package name, not an instance handle."
<code>packet h = new();
h::display(); // BUG: :: works on type names, not handles</code>The :: operator's left side must be a type name (class, package, or parameterised specialisation), not an instance handle. Handles are runtime values; :: is compile-time name lookup.
Use the dot operator for instance method calls: h.display();. Reserve :: for type-level access (static members, enum literals, package symbols, parameterised specialisations).
"Parameterised specialisation requires :: with full parameter list"
DEBUGCode referencing a parameterised class's static member without the parameter list compiles, but resolves to the default specialisation, not the intended one.
<code>class fifo #(type T = int);
static int instance_count = 0;
endclass
fifo#(byte) fb = new(); // instance of fifo#(byte)
$display(fifo::instance_count); // BUG: refers to fifo#(int)::instance_count
$display(fifo#(byte)::instance_count); // correct — fifo#(byte)::instance_count</code>A bare fifo::instance_count refers to the default specialisation (fifo#(int)), which has its own separate static. Static fields are per-specialisation in parameterised classes — each #(T) has its own counter.
Always include the full parameter list when accessing static members of a parameterised class: fifo#(byte)::instance_count. The qualification documents which specialisation you mean and eliminates the silent-bug case where the default specialisation is reached instead.
Interview Q&A — Twelve Questions You Will Be Asked
It is the scope resolution operator. It reaches into a type's scope — a class, a package, or a parameterised specialisation — to access a member of that scope by name. Common uses: ClassName::static_field, ClassName::static_method(), ClassName::ENUM_VALUE, package_name::symbol, and ParameterisedClass#(Type)::member.
Best Practices — Ten Rules for Using :: Well
- Always use
::for static members, even whenhandle.static_fieldwould compile. The qualified form documents class-wide semantics at the call site. - Use
::for enum literals declared inside a class.my_driver::S_IDLEis grep-friendly and self-documenting; a bareS_IDLEhides the owner. - Qualify package symbols with
::whenever ambiguity is possible. Even if no collision exists today, an explicituvm_pkg::uvm_objectsurvives every future package addition. - Prefer explicit imports over wildcard imports.
import my_pkg::base_xact;documents the dependency;import my_pkg::*;scales poorly as the codebase grows. - For parameterised classes, include the full parameter list when accessing static members.
fifo#(byte)::counter— never a barefifo::counter, which silently reaches the default specialisation. - Use
::with the UVM factory consistently.ClassName::type_id::create("name", parent)is the canonical construction form; anything else (directnew()) bypasses the factory and breaksset_type_override. - Never use
::on an instance handle.handle::method()is a compile error — the left side of::must be a type name. Usehandle.method()for instance calls. - Treat every
::in your code as documentation. A reader should be able to tell from the qualified form alone whether the reference is to a static field, an enum literal, a package symbol, or a parameterised specialisation. - Use qualified names in
$displayoutput during bring-up. Prefix log lines with$typename(this)::or the class name literally — turns hours of "which component is this from?" into a quick log scan. - When in doubt, qualify. The cost is a few extra characters; the benefit is documentation, disambiguation, and future-proofing against package additions or symbol collisions.