Skip to content

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

dot vs :: — Both in One Example
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Five 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

Use 1 — Accessing Static Property via ClassName::
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 phase

Use 2 — Static Method

Use 2 — Calling Static Method via ClassName::
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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());  // 2

Use 3 — Nested Class Type

Use 3 — Accessing Nested Class via OuterClass::
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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=1

Use 4 — typedef / enum Values Inside a Class

Use 4 — Accessing typedef and Enum Values via ClassName::
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Use 5 — Parameterised Class — Each Specialisation Has Its Own Static
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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) :: member

Complete Working Example — All Five Uses Together

Complete Example — All Five :: Uses in One Testbench
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════════════════════════════════════════════════════════════════
// 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

UseSyntaxObject needed?
Static propertyClassName::propNo
Static methodClassName::method(args)No
Nested classOuterClass::InnerClass obj = new();No (for type); Yes (for methods)
Enum type from inside classClassName::enum_type var;No
Enum value from inside classClassName::ENUM_VALUENo
Parameterised class staticClassName#(T)::memberNo
Instance property (dot)handle.propYes — 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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.

✅ Disambiguated with ::
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
❌ Ambiguous wildcard collision
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

1

"Compile error: 'PI' is an unknown identifier"

DEBUG
Symptom

A test references a package-level constant PI and gets an "unknown identifier" error, even though the constant is clearly defined in the imported package.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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

2

"Wildcard import collision — compiler refuses to pick"

DEBUG
Symptom

Two 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."

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>import uvm_pkg::*;
import my_pkg::*;        // my_pkg also exports uvm_sequence_item
 
uvm_sequence_item req;   // AMBIGUOUS — which one?</code>
Root Cause

Two packages exporting the same symbol via wildcard imports create an unresolvable name conflict. The compiler refuses to silently guess.

Fix

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.

3

"Static counter accessed via . through a handle silently works but confuses reviewers"

DEBUG
Symptom

Code compiles and runs, but a code review flags obj.next_id with the comment "I thought next_id was supposed to be class-wide?"

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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.

4

"handle::method() compile error — '::' requires a type name"

DEBUG
Symptom

A 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."

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>packet h = new();
h::display();    // BUG: :: works on type names, not handles</code>
Root Cause

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.

Fix

Use the dot operator for instance method calls: h.display();. Reserve :: for type-level access (static members, enum literals, package symbols, parameterised specialisations).

5

"Parameterised specialisation requires :: with full parameter list"

DEBUG
Symptom

Code referencing a parameterised class's static member without the parameter list compiles, but resolves to the default specialisation, not the intended one.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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

  1. Always use :: for static members, even when handle.static_field would compile. The qualified form documents class-wide semantics at the call site.
  2. Use :: for enum literals declared inside a class. my_driver::S_IDLE is grep-friendly and self-documenting; a bare S_IDLE hides the owner.
  3. Qualify package symbols with :: whenever ambiguity is possible. Even if no collision exists today, an explicit uvm_pkg::uvm_object survives every future package addition.
  4. Prefer explicit imports over wildcard imports. import my_pkg::base_xact; documents the dependency; import my_pkg::*; scales poorly as the codebase grows.
  5. For parameterised classes, include the full parameter list when accessing static members. fifo#(byte)::counter — never a bare fifo::counter, which silently reaches the default specialisation.
  6. Use :: with the UVM factory consistently. ClassName::type_id::create("name", parent) is the canonical construction form; anything else (direct new()) bypasses the factory and breaks set_type_override.
  7. Never use :: on an instance handle. handle::method() is a compile error — the left side of :: must be a type name. Use handle.method() for instance calls.
  8. 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.
  9. Use qualified names in $display output 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.
  10. 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.