Skip to content

SystemVerilog · Module 9

Nested Classes

Class-scoped helper types, qualified-name discipline, no implicit enclosing-instance access, and the namespace-hygiene wins of nesting class-local state enums.

Module 9 · Page 9.13

What Nested Classes Are

A nested class is simply a class declared inside another class. The inner class becomes scoped to the outer class — it is only visible through the outer class's name. Nothing else about it is special. It still has its own properties, methods, constructors, and lifecycle.

The main reason to nest a class is organisation. When a small helper class is tightly related to one outer class and makes no sense independently, putting it inside keeps the namespace clean and signals the relationship clearly. Nested Class Scope — How Visibility Worksclass ApbDriver rand bit [31:0] addr; Config cfg; class Config (nested inside ApbDriver) int clk_mhz = 100; int timeout = 1000; bit verbose = 0; Config is only accessible as ApbDriver::Config from outside. Inside ApbDriver's own methods, use it directly as Config.

Basic Syntax

Nested Class — Syntax and Scope Resolution
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Outer class ───────────────────────────────────────────────
class ApbDriver;
 
    // ── Nested (inner) class ──────────────────────────────────
    class Config;
        int  clk_mhz = 100;
        int  timeout  = 1000;
        bit  verbose  = 0;
 
        function void display();
            $display("Config: clk=%0d  timeout=%0d  verbose=%0b",
                      clk_mhz, timeout, verbose);
        endfunction
    endclass   // ← inner class ends here, inside outer
 
    // Outer class properties — use inner class name directly
    Config  cfg;    // short name works inside the outer class
    string  name;
 
    function new(string n = "drv");
        this.name = n;
        this.cfg  = new();   // create the nested Config object
    endfunction
 
    function void display();
        $display("[%s]", name);
        cfg.display();   // call inner object's method
    endfunction
 
endclass
 
// ── Accessing the inner class from OUTSIDE the outer class ────
module tb;
    ApbDriver drv;   // declare handle at module level
 
    initial begin
 
        // Use :: to access inner class through outer class name
        ApbDriver::Config my_cfg = new();
        my_cfg.clk_mhz = 200;
        my_cfg.verbose  = 1;
        my_cfg.display();
        // Config: clk=200  timeout=1000  verbose=1
 
        // Create via the outer object
        drv = new("apb_drv");
        drv.cfg.clk_mhz = 50; // ← can modify cfg directly
        drv.display();         //  ← prints driver + config info
 
    end
endmodule

Access Rules — What Can Touch What

This is the part that surprises most engineers. The inner class does not automatically have access to the outer class's instance properties. There is no implicit back-reference. If the inner class needs data from the outer class, you pass it explicitly.

Access Rules — What the Inner Class Can and Cannot See
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Outer;
 
    int         instance_var = 42;   // instance property
    static int  shared_count = 0;    // static property
    local int   secret       = 99;   // local — private to Outer
 
    class Inner;
        int inner_val;
 
        function void try_access();
            // instance_var  ← ERROR: no outer-class object context
            // secret        ← ERROR: local to Outer, Inner cannot see it
 
            // Static members ARE accessible — they belong to the class, not an object
            shared_count++;            // OK: Outer::shared_count
            $display("shared=%0d", shared_count);
        endfunction
    endclass
 
    // Outer class can pass its own data to Inner explicitly
    function void give_data_to_inner(Inner i);
        i.inner_val = instance_var;   // outer passes its own data
    endfunction
 
endclass
What the inner class tries to accessResult
Outer class instance property (instance_var)❌ Compile error — no implicit outer-object reference
Outer class local property (secret)❌ Compile error — local is private to the outer class only
Outer class static property (shared_count)✅ Allowed — static belongs to the class, not an object
Its own properties and methods✅ Normal — inner class is a full class
Outer data passed as a method argument✅ Allowed — explicit parameter, no scoping issue

typedef Inside a Class — A Cleaner Pattern

Instead of nesting a full class, you can define a typedef inside a class. This gives the outer class a locally-scoped type alias without the weight of a full nested class definition. It is lighter and more common for simple type aliases.

typedef Inside a Class — Scoped Type Aliases
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class AhbTransaction;
 
    // typedef scoped to this class — cleaner than a nested class
    typedef enum bit [2:0] {
        SINGLE  = 3'b000,
        INCR    = 3'b001,
        WRAP4   = 3'b010,
        INCR4   = 3'b011,
        WRAP8   = 3'b100,
        INCR8   = 3'b101
    } burst_e;
 
    typedef enum bit [1:0] {
        OKAY   = 2'b00,
        ERROR  = 2'b01,
        RETRY  = 2'b10,
        SPLIT  = 2'b11
    } resp_e;
 
    rand burst_e    hburst;
    rand bit [31:0] haddr;
    resp_e          hresp;
 
    function void display();
        $display("[AHB] burst=%s  addr=0x%08h  resp=%s",
                  hburst.name(), haddr, hresp.name());
    endfunction
 
endclass
 
// Access typedef from outside via :: operator
AhbTransaction::burst_e b = AhbTransaction::INCR4;
$display("burst = %s", b.name());   // burst = INCR4

Practical Verification Use Cases

Use Case 1 — Driver with Nested Config

Group all configuration knobs for a driver into a nested Config class. All the settings live in one place, easy to read and extend. The outer driver creates a Config object in its constructor and uses it through the property.

Nested Config Class — VCS Verified
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class SpiDriver;
 
    // ── Nested class — defined inside SpiDriver's scope ───────
    class Config;
        int       clk_mhz   = 10;
        bit [1:0] spi_mode  = 2'b00;   // CPOL=0 CPHA=0
        int       cs_delay  = 2;
        bit       lsb_first = 0;
        int       word_size = 8;
 
        function void display();
            $display("[SPI Config] clk=%0dMHz  mode=%02b  word=%0db  %s",
                      clk_mhz, spi_mode, word_size,
                      lsb_first ? "LSB" : "MSB");
        endfunction
    endclass   // ← Config ends here, still inside SpiDriver
 
    // ── Outer class properties ────────────────────────────────
    Config  cfg;    // use short name inside the outer class
    string  name;
 
    // ── Constructor: create the inner Config object here ──────
    function new(string n = "spi_drv");
        name = n;
        cfg  = new();   // creates the nested Config object
    endfunction
 
    function void display();
        $display("Driver: %s", name);
        cfg.display();   // call the inner object's method
    endfunction
 
endclass
 
module tb;
SpiDriver drv1, drv2;
SpiDriver::Config standalone_cfg;
    initial begin
 
        // ── Simple usage — default config ─────────────────────
         drv1 = new();
        drv1.display();
        // Driver: spi_drv
        // [SPI Config] clk=10MHz  mode=00  word=8b  MSB
 
        // ── Modify config via the outer object's cfg property ─
         drv2 = new("fast_drv");
        drv2.cfg.clk_mhz   = 50;
        drv2.cfg.lsb_first = 1;
        drv2.display();
        // Driver: fast_drv
        // [SPI Config] clk=50MHz  mode=00  word=8b  LSB
 
        // ── Create a standalone Config via outer class scope ──
        standalone_cfg = new();
        standalone_cfg.clk_mhz = 100;
        standalone_cfg.display();
        // [SPI Config] clk=100MHz  mode=00  word=8b  MSB
 
    end
endmodule

Use Case 2 — Transaction with Nested Status

A transaction class often needs a small status sub-object — populated after the DUT responds. Nesting it keeps the response fields logically separate from the stimulus fields while keeping everything in one file.

Nested Status — Stimulus and Response in One Transaction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class I2cTransaction;
 
    // ── Nested status object — filled by the monitor after drive ─
    class Status;
        bit   ack_received;
        bit   arb_lost;
        bit   timeout;
        int   cycles_taken;
 
        function bit is_success();
            return (ack_received && !arb_lost && !timeout);
        endfunction
 
        function void display();
            $display("  Status: ack=%0b  arb=%0b  tout=%0b  cycles=%0d  [%s]",
                      ack_received, arb_lost, timeout,
                      cycles_taken,
                      is_success() ? "OK" : "FAIL");
        endfunction
    endclass
 
    // ── Stimulus fields ───────────────────────────────────────
    rand bit [6:0]  slave_addr;
    rand bit [7:0]  reg_addr;
    rand bit [7:0]  data;
    bit              read_write;    // 0=write 1=read
    int              txn_id;
 
    // ── Response — created automatically in constructor ────────
    Status           status;
 
    function new(int id = 0);
        txn_id = id;
        status = new();   // always create status object
    endfunction
 
    function void display();
        $display("[I2C #%0d] %s  slave=0x%02h  reg=0x%02h  data=0x%02h",
                  txn_id,
                  read_write ? "RD" : "WR",
                  slave_addr, reg_addr, data);
        status.display();
    endfunction
 
endclass
 
module tb;
    I2cTransaction t = new(1);
    initial begin
        void'(t.randomize());
        // Simulate DUT filling in status
        t.status.ack_received = 1;
        t.status.cycles_taken = 18;
        t.display();
    end
endmodule

Nested vs Separate Class — Which One to Use

Use nested class when

  • The inner class only makes sense in the context of the outer class
  • The inner class is never used anywhere outside the outer class
  • It is a small helper (Config, Status, Stats) with no independent life
  • You want to keep the global namespace clean Use separate class when
  • The class is used by multiple other classes
  • It is large enough to deserve its own file
  • It participates in an inheritance hierarchy
  • Other teams need to extend or import it independently

Quick Reference

OperationSyntaxNotes
Declare nested classclass Inner; ... endclass inside outer class bodyInner class is scoped to outer class
Use inner class inside outerInner obj = new();Short name works inside the outer class
Access inner class from outsideOuterClass::InnerClass obj = new();Scope resolution :: required
Inner accesses outer staticOuterClass::static_prop or just static_prop✅ Allowed — static has no object context
Inner accesses outer instance propertyNot possible without an explicit outer handle❌ No implicit outer-object reference
Inner accesses outer local propertyNot possiblelocal blocks even nested classes
typedef inside a classtypedef enum {...} my_e; inside class bodyLighter alternative to nested class for type aliases

Verification Usage — Where Nested Classes Earn Their Place in Real Testbenches

Nested classes are a sharp tool that solves a specific problem: keeping the global namespace clean and the relationship between a helper and its owner unambiguous. Three patterns recur across production UVM environments.

State enums and bookkeeping types scoped to one class

A driver's internal state machine, a sequence's phase enum, a monitor's error codes — all of these have meaning only inside their owning class. Defining them as nested typedef enum makes the relationship explicit and prevents pollution of the global namespace.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class apb_driver extends uvm_driver #(apb_xact);
  typedef enum {S_IDLE, S_SETUP, S_ACCESS, S_DONE} state_e;
  typedef enum {E_NONE, E_TIMEOUT, E_PSLVERR}      error_e;
 
  state_e state;
  error_e last_error;
endclass
 
// External code refers via the qualified name:
apb_driver::state_e s = apb_driver::S_IDLE;</code>

Private helper classes used by exactly one outer class

A scoreboard might need a small txn_record helper class to store one expected transaction plus metadata (timestamp, source agent, prediction confidence). If nothing else in the testbench ever uses txn_record, nesting it inside the scoreboard prevents misuse and makes the ownership clear at a glance.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class my_scoreboard extends uvm_scoreboard;
 
  // Private bookkeeping type, used by no one else
  class txn_record;
    apb_xact xact;
    time     received_at;
    int      source_agent_id;
 
    function new(apb_xact x, int aid);
      xact            = x;
      received_at     = $time;
      source_agent_id = aid;
    endfunction
  endclass
 
  txn_record expected[$];
endclass</code>

Coverage-collection helpers that mirror one transactor's view

A monitor that owns a covergroup often also owns a small helper class that pre-processes sampled values before calling cg.sample(). Nesting the helper inside the monitor keeps the per-bus-protocol coverage logic encapsulated — different protocols can each have their own scoped helper without any naming collisions.

Simulation Behavior — What the Simulator Does With Nested Classes

Nesting is purely a compile-time naming convention

The simulator does not store nested classes any differently from top-level classes — at runtime they are ordinary objects with ordinary vtables. The only difference is at compile time: the nested class's fully-qualified name is outer::inner, and all access-control rules are checked relative to that scope.

No implicit pointer to an enclosing instance

A nested class in SystemVerilog is more like a C++ nested class than a Java inner class — it has no hidden field pointing to "the outer object that created me." If the nested class needs to access outer-instance state, the outer must pass this explicitly to the nested constructor and the nested must store it as an ordinary field.

Static members of nested classes are normal static

A static int declared inside a nested class has exactly one storage cell, addressed as outer::inner::field_name. The nesting affects the qualified name only — the underlying lifetime and sharing semantics are identical to a top-level class's static fields.

Construction follows ordinary scoping rules

You can construct an instance of a nested class anywhere the qualified name is visible: outer::inner h = new();. Inside the outer class's own methods you may write just inner h = new();. The qualified form is required from any other scope.

✅ Nested helper — clean scoping
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class scoreboard;
  class record;
    apb_xact x;
    time     t;
  endclass
 
  record entries[$];
 
  function void log(apb_xact xact);
    record r = new();
    r.x = xact; r.t = $time;
    entries.push_back(r);
  endfunction
endclass
 
// External: scoreboard::record is the qualified name
// Top-level namespace is uncluttered</code>
❌ Top-level helper — pollutes namespace
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class record;           // floating helper
  apb_xact x;
  time     t;
endclass
 
class scoreboard;
  record entries[$];
 
  function void log(apb_xact xact);
    record r = new();
    r.x = xact; r.t = $time;
    entries.push_back(r);
  endfunction
endclass
 
// 'record' lives in the global namespace —
// likely to collide with another component's
// 'record' helper.</code>

Waveform Analysis — Tracing Nested Class Activity in Debug

Nested classes leave no special waveform signature — they're ordinary classes. The only debug-time consideration is that $typename() on a nested-class handle returns the qualified name, which makes ownership immediately readable.

Type identification by qualified name

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class scoreboard;
  class record;
    apb_xact x;
    time     t;
  endclass
  record entries[$];
 
  function void show();
    foreach (entries[i])
      $display("[%0t] entry %s @ %0t  xact=%s",
               $time, $typename(entries[i]), entries[i].t,
               $typename(entries[i].x));
  endfunction
endclass
 
// Log output:
//   100ns entry scoreboard::record @ 50  xact=apb_xact
//   150ns entry scoreboard::record @ 95  xact=apb_xact
//   200ns entry scoreboard::record @ 145 xact=apb_xact</code>

ASCII view — qualified types in mixed-component log

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>100ns  scoreboard::record   created  @ 50    xact=apb_xact id=1
150ns  monitor::sample      fired    @ 95    xact=apb_xact id=2
200ns  scoreboard::record   created  @ 145   xact=apb_xact id=2
       ↑                    ↑
   nested helper        method on outer
   instantly identifies its parent class</code>

Industry Insights — Hard-Won Lessons About Nested Classes

Debugging Academy — Five Real Bugs From Nested Class Usage

1

"Nested class can't see outer instance fields"

DEBUG
Symptom

Code inside a nested class references an outer instance field and the compiler rejects it with "undeclared identifier 'name'".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class scoreboard;
  string name = "sb";
 
  class record;
    function void show();
      $display("from %s", name);   // BUG: no implicit outer-instance access
    endfunction
  endclass
endclass</code>
Root Cause

SystemVerilog nested classes have no implicit pointer to an enclosing instance. The nested class can see the outer class's type declarations and static members, but it cannot reach instance fields of the outer without an explicit handle.

Fix

Pass a handle to the outer through the nested constructor: class record; scoreboard m_parent; function new(scoreboard p); m_parent = p; endfunction function void show(); $display("from %s", m_parent.name); endfunction endclass.

2

"External code can't find the nested type — 'record' is not declared"

DEBUG
Symptom

Code outside the outer class references the nested type by its short name and gets "undeclared identifier 'record'".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class scoreboard;
  class record;
    apb_xact x;
  endclass
endclass
 
module tb;
  initial begin
    record r = new();   // BUG: needs qualified name
  end
endmodule</code>
Root Cause

Outside the enclosing class, the nested type is only visible by its fully-qualified name. The bare name record is meaningless in the module scope.

Fix

Always use the qualified name from external scopes: scoreboard::record r = new();. If you find yourself reaching for the type often from outside, that's a signal the type should be promoted to top-level.

3

"Nested class can't access local fields of its outer"

DEBUG
Symptom

A nested class is given an outer handle and tries to read an outer field declared local — compile error: "cannot access local member".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class scoreboard;
  local int secret_count = 0;       // outer-private
 
  class record;
    scoreboard m_parent;
    function new(scoreboard p); m_parent = p; endfunction
 
    function void show();
      $display("count=%0d", m_parent.secret_count);   // ILLEGAL
    endfunction
  endclass
endclass</code>
Root Cause

A nested class is not a friend of its outer in SystemVerilog. It has no special access to local members of the outer — only to public ones (and to outer protected members through inheritance, which is not what nesting provides).

Fix

Either (a) widen the outer field's access to protected or expose a getter, or (b) provide an accessor method on the outer that the nested class calls: function int get_secret_count(); return secret_count; endfunction.

4

"Forward reference fails — nested A needs nested B which is declared later"

DEBUG
Symptom

A nested class A holds a handle to nested class B, both inside the same outer. Compile error: "undeclared type 'B'".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class scoreboard;
  class A;
    B b;            // BUG: B not yet declared
  endclass
 
  class B;
    int val;
  endclass
endclass</code>
Root Cause

Declaration order matters within an outer class — at the point where A references B, B has not yet been parsed.

Fix

Use a forward declaration with typedef class at the top of the outer: typedef class B; class A; B b; endclass class B; int val; endclass. The forward declaration tells the compiler that B is a class type that will be fully defined later.

5

"Nested static counter behaves per-outer-instance, not class-wide as expected"

DEBUG
Symptom

A nested class's static int count is expected to be a single shared counter, but each outer instance appears to have its own counter starting from zero.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class outer;
  class inner;
    static int count = 0;     // expected to be class-wide
    function new();
      count++;
    endfunction
  endclass
endclass
 
// In test: two outer instances each create an inner.
// Expectation: outer::inner::count == 2
// Reality:    outer::inner::count == 2  ← actually correct!
// But user observation: "each outer's view shows 1"</code>
Root Cause

Actually the static is class-wide — there is exactly one outer::inner::count for the whole simulation. The misobservation typically comes from looking at the counter through different outer instances and confusing oneself. The static is correctly singular.

Fix

Always reference statics through the qualified name (outer::inner::count) rather than through a particular instance, even though both forms work. The qualified form makes the "one shared cell" semantics visually obvious and prevents the kind of cognitive confusion that produces a non-existent bug report.

Interview Q&A — Twelve Questions You Will Be Asked

A class declared inside the body of another class. The nested class's fully-qualified name includes the outer's name (outer::inner), and it is referenced that way from outside the outer's scope. Nesting is purely a compile-time organisational tool — at runtime, nested classes behave exactly like top-level classes.

Best Practices — Ten Rules for Using Nested Classes Well

  1. Nest a helper class only if it is used by exactly one outer class. If two or more outer classes reach for it, promote the helper to top-level.
  2. Always prefer nested typedef enum over global enums for class-local state machines. The qualified name (my_driver::S_IDLE) makes ownership self-documenting.
  3. Pass an outer-instance handle to the nested constructor when the nested class needs outer-instance state. SystemVerilog does not provide automatic enclosing-instance access — the link must be explicit.
  4. Do not assume nested classes are "friends" of their outer. They cannot reach the outer's local members; expose accessors or widen access if needed.
  5. Reference nested classes from external code by qualified name only. outer::inner is the contract; the short name is invisible from outside.
  6. Use typedef class X; for forward declarations when nested classes reference each other. The same mechanism that works for top-level forward declarations works for nested ones.
  7. Keep nested classes small. Once a nested class grows beyond ~50 lines, it usually deserves its own file — file boundaries are good thinking-units even when the namespace boundary isn't.
  8. Treat nested static members as ordinary statics. Address them through the qualified name (outer::inner::count); avoid accessing through an outer instance to keep the "one shared cell" semantics visible.
  9. Don't nest a class purely to "make it private." SystemVerilog has no private equivalent for classes; nesting hides the name from the global scope but does not prevent any code that can write the qualified name from using it.
  10. Print $typename(this) in nested-class methods during bring-up. The qualified name in log lines instantly identifies the owning component — one of the under-appreciated debug wins of nesting.