Why Learn the Scope Resolution Operator? As a testbench grows to include multiple classes, packages, and reused names across different files, it becomes entirely possible for two completely different things to share the exact same identifier — two different meanings of "TRUE," two different classes each with their own "data" field. The scope resolution operator, ::, is how SystemVerilog lets you say precisely which one you mean, instead of leaving it to guesswork or accidental shadowing.

The scope resolution operator :: is used to refer to an identifier within the scope of a class, package, covergroup, coverpoint, cross, or typedef name. The left-hand side must be one of those scope names; the right-hand side is an identifier inside that scope, like a variable or method name.

7 min read | Intermediate Level

What You'll Learn

  • What :: does, and what's legal on each side of it
  • How to define a class method's body outside the class declaration itself
  • How :: accesses a class's static members without needing an object
  • How to reference a package's members directly, with or without importing the package
  • How :: resolves a naming collision between an imported package and a local declaration

Why Is the Scope Resolution Operator Required?

Classes and other scopes can have identically-named identifiers, which can create a namespace collision if referred to without specifying which scope you mean. :: uniquely identifies a member or parameter of a given scope. It's also used to access a class's static variables and methods, parameters, and local parameters from outside the class, and to access public and protected members of a base class from within a derived class.

Defining an extern Function

A method can be declared inside a class with extern, and its actual body defined separately, outside the class, using :: to say which class it belongs to:


class ABC;
	int 	data;

	extern virtual function void display();
endclass

// Definition of an external function using scope
// resolution operator
function void ABC::display();
	$display("data = 0x%0h", data);
endfunction

module tb;
	initial begin
		ABC abc = new();
		abc.data = 32'hface_cafe;
		abc.display();
	end
endmodule

Output

Output
ncsim> run
data = 0xfacecafe
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

ABC::display() is what tells the compiler "this function body belongs to the display() method declared inside ABC," even though it's written outside the class entirely.

Quick Check

Q: What would happen if function void ABC::display();...endfunction were never written anywhere in the code?

Show Answer

A compile error. extern only declares that the method exists and what its signature looks like — the actual body still has to be provided somewhere, using :: to tie it back to the class.

Accessing Static Methods and Variables

A class's static members belong to the class itself, not to any one object, and :: lets you reach them without creating an instance first:


class ABC;
	static int 	data;

	static function void display();
		$display("data = 0x%0h", data);
	endfunction
endclass

module tb;
	initial begin
      	ABC a1, a2;

      	// Assign to static variable before creating
      	// class objects, and display using class_type and
      	// scope resolution operator
		ABC::data = 32'hface_cafe;
		ABC::display();
  
      	a1 = new();
      	a2 = new();
      	$display ("a1.data=0x%0h a2.data=0x%0h", a1.data, a2.data);
	end
endmodule

Output

Output
ncsim> run
data = 0xfacecafe
a1.data=0xfacecafe a2.data=0xfacecafe
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

ABC::data is assigned before either a1 or a2 even exists, and both objects still see the same value once created — exactly what static means: one shared copy for the whole class, not one per object. See SystemVerilog Static Variables & Functions for more on this.

Quick Check

Q: If a third object, a3 = new(), were created right after this code, and then a1.data were changed to a new value, what would a3.data show?

Show Answer

The new value. Since data is static, a1.data, a2.data, and a3.data are all just different names for the exact same underlying storage — changing it through any one of them changes what all of them see.

Referencing Package Members

:: can also reach directly into a package, referencing a type or parameter declared there without importing the whole package first — the package just needs to be included in compilation:


package my_pkg;
	typedef enum bit {FALSE, TRUE} e_bool;
endpackage

module tb;
  bit val;
  
  initial begin
  	// Refer to types that have been declared
  	// in a package. Note that package has to
  	// be included in compilation but not
  	// necessarily "imported"
    val = my_pkg::TRUE;
    $display("val = 0x%0h", val);
  end
endmodule

Output

Output
ncsim> run
val = 0x1
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

my_pkg::TRUE reaches directly into the package to grab its TRUE enum value (1, since it's the second value declared in FALSE, TRUE) without an import statement anywhere.

Avoiding Namespace Collisions

Things get more interesting when a locally-declared identifier shares a name with something already imported from a package:


package my_pkg;
	typedef enum bit {FALSE, TRUE} e_bool;
endpackage

import my_pkg::*;

module tb;
  typedef enum bit {TRUE, FALSE} e_bool;
  
  initial begin
    e_bool val;
  
    // Be explicit and say that TRUE from my_pkg
    // should be assigned to val
    val = my_pkg::TRUE;
    $display("val = 0x%0h", val);
  
    // TRUE from current scope will be assigned to
    // val
    val = TRUE;
    $display("val = 0x%0h", val);
  end
endmodule

Output

Output
ncsim> run
val = 0x1
val = 0x0
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

Notice the local e_bool reverses the order: TRUE, FALSE instead of the package's FALSE, TRUE — so the module's own TRUE is 0, while the package's TRUE is 1. val = my_pkg::TRUE explicitly forces the package's version, giving 0x1. val = TRUE, with no qualification, resolves to the module's own local declaration instead — giving 0x0 — because a local declaration takes precedence over a wildcard-imported (import my_pkg::*;) one, even though both are visible in the same scope.

Quick Check

Q: If the module's local typedef were removed entirely, what would val = TRUE; (unqualified) resolve to?

Show Answer

The package's TRUE (value 1). With no local declaration to shadow it, the wildcard-imported my_pkg::* version becomes the only TRUE visible in that scope, so the unqualified reference resolves to it.

Where Does This Matter in Practice?

Large verification environments commonly share types, parameters, and enums across many files through packages, often pulled in with a wildcard import for convenience. Relying on that convenience everywhere means a locally-declared identifier with the same name can silently shadow the package's version with no warning — explicit :: qualification is the way to be certain which one is actually being used when it matters.

Common Beginner Mistakes

Mistake #1: Assuming a wildcard import behaves exactly like a local declaration

As the namespace collision example shows, a local declaration silently wins over a wildcard-imported identifier with the same name. Why? This can produce a value that looks wrong at first glance if you forgot a local declaration existed and expected the package's version to be used instead — explicit :: removes the ambiguity entirely.

Mistake #2: Forgetting to actually define an extern-declared method

extern only declares a method's signature inside the class — it doesn't provide a body. Why? If ClassName::method_name(...) is never written anywhere, the method has no implementation, and you'll get a compile error rather than a working (if empty) function.

Mistake #3: Thinking :: is only for static members

:: shows up in several different contexts — defining an extern method's body, accessing static members, reaching into a package, and accessing base-class members from a derived class. Why? Treating it as purely a "static member" operator misses how often it's actually used just to disambiguate which scope an identifier belongs to.

Practice Exercise

Challenge: Trace a Different Collision

Given package p; typedef enum bit [1:0] {RED, GREEN, BLUE, YELLOW} e_color; endpackage, imported with import p::*;, and a module that locally declares typedef enum bit [1:0] {YELLOW, BLUE, GREEN, RED} e_color;, what would e_color c; c = RED; (unqualified) evaluate to, and what would c = p::RED; evaluate to?

Hint

Work out each enum's ordinal values separately first — the package's declaration order, and the module's local declaration order — then apply the same shadowing rule from the article.

Solution

c = RED; gives 3, using the local declaration (YELLOW=0, BLUE=1, GREEN=2, RED=3), since the local typedef shadows the wildcard import. c = p::RED; gives 0, explicitly forcing the package's own ordering (RED=0, GREEN=1, BLUE=2, YELLOW=3) regardless of what's declared locally.

Summary

In this tutorial, you learned:

  • :: resolves an identifier to a specific scope — a class, package, covergroup, coverpoint, cross, or typedef name on its left-hand side
  • An extern-declared method's body is defined outside the class using ClassName::method_name
  • A class's static members are accessed with ClassName::member_name, without needing an object instance
  • A package's members can be referenced directly with package_name::member_name, even without importing the package
  • A local declaration silently shadows a same-named identifier pulled in through a wildcard import — explicit :: qualification removes that ambiguity

Continuing the Advanced Features Chapter

Continue with SystemVerilog 'extern' for a deeper look at declaring methods outside a class body.

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5