Polymorphism in SystemVerilog

Polymorphism is one of the most powerful concepts in Object-Oriented Programming. It allows you to write flexible code that can work with different types of objects in a unified way. In this tutorial, we'll break down polymorphism in simple terms and show you exactly how it works in SystemVerilog verification.

What is Polymorphism?

The word "polymorphism" comes from Greek and means "many forms". In programming, it means that a single method or function can behave differently depending on which object is calling it.

Think of it like a TV remote's "power" button. The same button can turn on a Samsung TV, an LG TV, or a Sony TV - but each TV responds in its own way (different startup screen, different sound, etc.). The command is the same, but the behavior depends on the device.

Why is Polymorphism Important?

In verification, polymorphism helps you:

  • Write reusable code - Same testbench code can test different DUT configurations
  • Extend functionality easily - Add new transaction types without changing existing code
  • Create flexible test environments - Swap components at runtime
  • Build UVM testbenches - UVM heavily relies on polymorphism for its architecture

Key Insight

Without polymorphism, you would need to write separate code for every type of transaction or component. With polymorphism, one piece of code handles them all!

The Virtual Keyword

The secret to polymorphism in SystemVerilog is the virtual keyword. When you declare a method as virtual, you're telling SystemVerilog: "The actual method to call should be determined at runtime, based on the actual object type."

Without Virtual (Static Binding)

Without virtual, the method that gets called is determined by the variable type at compile time. This is called static binding.

Without Virtual - Static Binding
class Animal;
    function void speak();
        $display("Some generic animal sound");
    endfunction
endclass

class Dog extends Animal;
    function void speak();
        $display("Woof! Woof!");
    endfunction
endclass

// In testbench:
Animal a;
Dog d = new();
a = d;        // Parent handle pointing to child object
a.speak();    // Prints: "Some generic animal sound" - NOT what we want!

With Virtual (Dynamic Binding)

With virtual, the method that gets called depends on the actual object type at runtime. This is called dynamic binding or late binding.

With Virtual - Dynamic Binding
class Animal;
    virtual function void speak();
        $display("Some generic animal sound");
    endfunction
endclass

class Dog extends Animal;
    virtual function void speak();  // 'virtual' optional in child, but good practice
        $display("Woof! Woof!");
    endfunction
endclass

// In testbench:
Animal a;
Dog d = new();
a = d;        // Parent handle pointing to child object
a.speak();    // Prints: "Woof! Woof!" - Correct!

Pro Tip

Always use virtual for methods that might be overridden in child classes. In UVM, almost all methods are virtual!

Practical Example: Transaction Types

Let's see a real-world verification example. Imagine you have different types of transactions: read, write, and burst transactions. They all need to be printed and compared, but each has its own format.

Base Transaction Class
class Transaction;
    rand bit [31:0] addr;
    rand bit [31:0] data;
    
    // Virtual methods - child classes will override these
    virtual function void display();
        $display("Transaction: addr=0x%h, data=0x%h", addr, data);
    endfunction
    
    virtual function bit compare(Transaction t);
        return (this.addr == t.addr) && (this.data == t.data);
    endfunction
endclass
Child Transaction Classes
class ReadTransaction extends Transaction;
    rand bit [3:0] burst_len;
    
    virtual function void display();
        $display("READ Transaction: addr=0x%h, burst=%0d", addr, burst_len);
    endfunction
endclass

class WriteTransaction extends Transaction;
    rand bit [3:0] strobe;
    
    virtual function void display();
        $display("WRITE Transaction: addr=0x%h, data=0x%h, strobe=0x%h", 
                 addr, data, strobe);
    endfunction
endclass
Using Polymorphism
// Array of base class handles
Transaction trans_queue[$];

// Add different transaction types to the same queue
ReadTransaction  rd = new();
WriteTransaction wr = new();

rd.randomize();
wr.randomize();

trans_queue.push_back(rd);
trans_queue.push_back(wr);

// Print all transactions - each calls its own display()!
foreach(trans_queue[i]) begin
    trans_queue[i].display();  // Correct method called automatically
end

What's Happening Here?

Even though trans_queue is an array of Transaction handles, when we call display(), SystemVerilog looks at the actual object type and calls the right version. This is polymorphism in action!

How Polymorphism Works in UVM

UVM uses polymorphism everywhere. The most common example is the uvm_sequence_item class. All your transactions extend from it, and UVM components can work with any transaction type through the base class handle.

  • Sequences - One sequencer can handle different transaction types
  • Drivers - Virtual methods let you customize driving behavior
  • Monitors - Override methods to capture DUT-specific protocols
  • Scoreboards - Compare transactions using polymorphic compare methods

Common Mistake

Forgetting the virtual keyword is a very common bug. If your overridden method isn't being called, check if the parent method is declared as virtual!

Quick Summary

  • Polymorphism = "many forms" - same method, different behavior based on object type
  • Use the virtual keyword to enable polymorphism
  • Without virtual → static binding (method decided at compile time)
  • With virtual → dynamic binding (method decided at runtime)
  • Essential for UVM and flexible testbench design

What's Next?

Now that you understand polymorphism, you're ready to explore more advanced SystemVerilog concepts. We recommend: