Module 1: Polymorphism and the Virtual Dispatch Model
Polymorphism in SystemVerilog
What is Polymorphism and Why Does It Matter?
Polymorphism literally means "many forms." In OOP, it means one handle can refer to objects of different types, and calling a method on that handle executes the correct version for the actual object — not the declared type of the handle.
Without polymorphism, you'd write:
if (txn_type == READ) read_checker.check(txn);
else if (txn_type == WRITE) write_checker.check(txn);
else if (txn_type == BURST) burst_checker.check(txn);With polymorphism, you write:
checker.check(txn); // correct check() is called automaticallyThe scoreboard doesn't need to know what kind of transaction it received. It calls check() and the right implementation runs.
Analogy: Think of a pay() method on different payment types. Whether you call pay() on a CreditCard, DebitCard, or PayPal object, the right payment processing happens. The checkout system doesn't need to know the specific type — it just calls pay(). The object knows what it is and does the right thing.
In UVM, everything runs on polymorphism. The UVM factory, TLM analysis ports, scoreboards, sequences — all use base class handles and virtual methods to stay generic while delegating behavior to specific types.
The virtual Keyword — The Key to Runtime Dispatch
Without virtual, method calls are resolved at compile time based on the declared handle type (static dispatch). With virtual, calls are resolved at runtime based on the actual object type (dynamic dispatch).
class Animal;
string name;
function new(string n); name = n; endfunction
// NON-virtual: resolved at compile time from handle type
function void non_virtual_speak();
$display("%s: (generic animal sound)", name);
endfunction
// VIRTUAL: resolved at runtime from object type
virtual function void speak();
$display("%s: (generic animal sound)", name);
endfunction
endclass
class Dog extends Animal;
function new(string n); super.new(n); endfunction
function void non_virtual_speak();
$display("%s: Woof!", name);
endfunction
virtual function void speak(); // overrides parent's virtual
$display("%s: Woof!", name);
endfunction
endclass
class Cat extends Animal;
function new(string n); super.new(n); endfunction
function void non_virtual_speak();
$display("%s: Meow!", name);
endfunction
virtual function void speak();
$display("%s: Meow!", name);
endfunction
endclass// Static dispatch (non-virtual)
Animal a_handle;
Dog d = new("Rex");
Cat c = new("Whiskers");
a_handle = d;
a_handle.non_virtual_speak(); // "Rex: (generic animal sound)" — Animal's version!
// because handle is type Animal → compile-time binding
// Dynamic dispatch (virtual)
a_handle = d;
a_handle.speak(); // "Rex: Woof!" — Dog's version!
// runtime looks at ACTUAL object → Dog
a_handle = c;
a_handle.speak(); // "Whiskers: Meow!" — Cat's version!Why this is so powerful: a_handle is declared as Animal. It doesn't know at compile time whether it holds a Dog or Cat. With virtual, the runtime checks the actual type and dispatches to the right speak(). Change the object a_handle points to and the behavior changes automatically.
Building Polymorphic Systems
The Pattern: Base Handle + Virtual Methods + Array
// Heterogeneous collection of objects behind base class handles
Animal zoo[$];
zoo.push_back(new Dog("Rex"));
zoo.push_back(new Cat("Whiskers"));
zoo.push_back(new Dog("Buddy"));
zoo.push_back(new Cat("Luna"));
// One loop drives all — no type checks needed
foreach (zoo[i])
zoo[i].speak();
// Output:
// Rex: Woof!
// Whiskers: Meow!
// Buddy: Woof!
// Luna: Meow!Why this matters in verification: A scoreboard holds a queue of Transaction handles. Transactions can be reads, writes, or bursts. Each has a different check() method. The scoreboard's loop is just foreach (expected[i]) expected[i].check(actual[i]) — one generic loop, type-correct behavior on every element.
Polymorphism in a Scoreboard
// Base transaction — defines the interface
class Transaction;
logic [31:0] addr;
virtual function bit check(Transaction actual);
$display("Base check — should be overridden");
return 1;
endfunction
virtual function void print();
$display("Txn: addr=%0h", addr);
endfunction
endclass
// Read transaction
class ReadTxn extends Transaction;
logic [31:0] rdata;
virtual function bit check(Transaction actual_base);
ReadTxn actual;
if (!$cast(actual, actual_base)) return 0; // safe downcast
if (addr !== actual.addr || rdata !== actual.rdata) begin
$error("READ MISMATCH: exp_addr=%0h got_addr=%0h exp_data=%0h got_data=%0h",
addr, actual.addr, rdata, actual.rdata);
return 0;
end
return 1;
endfunction
endclass
// Write transaction
class WriteTxn extends Transaction;
logic [31:0] wdata;
logic [3:0] strobe;
virtual function bit check(Transaction actual_base);
WriteTxn actual;
if (!$cast(actual, actual_base)) return 0;
if (addr !== actual.addr || wdata !== actual.wdata) begin
$error("WRITE MISMATCH: ...");
return 0;
end
return 1;
endfunction
endclass
// Generic scoreboard — works with ANY Transaction subclass
class Scoreboard;
Transaction expected_q[$];
function void add_expected(Transaction t);
expected_q.push_back(t);
endfunction
function void check_actual(Transaction actual);
Transaction exp = expected_q.pop_front();
if (!exp.check(actual)) // polymorphic call — right check() runs
$error("Scoreboard mismatch!");
else
$display("PASS");
endfunction
endclassThe scoreboard stores Transaction handles but never needs to know the specific type. exp.check(actual) dispatches to ReadTxn::check or WriteTxn::check automatically based on the runtime type of exp.
virtual Task — Polymorphism Across Time
Virtual methods can also be tasks (time-consuming). This is how UVM phases work:
class uvm_component;
virtual task run_phase(uvm_phase phase);
// Default: does nothing
endtask
endclass
class my_driver extends uvm_component;
virtual task run_phase(uvm_phase phase);
// Your actual driver logic
forever begin
drive_next_item();
end
endtask
endclass
// UVM calls run_phase on all components polymorphically:
uvm_component comp_list[$];
// ... populate with drivers, monitors, scoreboards ...
foreach (comp_list[i])
fork
comp_list[i].run_phase(phase); // each calls its own run_phase
join_noneUVM's phase runner holds uvm_component handles. It calls run_phase() on each — and each component runs its own implementation. This is the entire UVM execution model.
virtual class — Abstract Base Classes
A virtual class (abstract class) cannot be instantiated — it exists only to define an interface for subclasses:
virtual class BaseProtocolChecker;
// Pure virtual — subclass MUST override this
pure virtual function void check_addr(logic [31:0] addr);
pure virtual function void check_data(logic [31:0] data);
// Concrete method — shared by all subclasses
function void report_pass();
$display("[%0t] CHECK PASSED", $time);
endfunction
endclass
class AXI4LiteChecker extends BaseProtocolChecker;
function void check_addr(logic [31:0] addr);
if (addr[1:0] != 2'b00)
$error("AXI4L: unaligned addr %0h", addr);
else
report_pass(); // inherited concrete method
endfunction
function void check_data(logic [31:0] data);
// AXI4L-specific data checks
endfunction
endclass
class APBChecker extends BaseProtocolChecker;
function void check_addr(logic [31:0] addr);
if (addr > 32'hFFFF)
$error("APB: addr %0h out of range", addr);
else
report_pass();
endfunction
function void check_data(logic [31:0] data);
// APB-specific checks
endfunction
endclass
// Usage: store different checker types behind base handle
BaseProtocolChecker checker;
checker = new AXI4LiteChecker();
checker.check_addr(32'h1001); // AXI4L check runs
checker = new APBChecker();
checker.check_addr(32'h10000); // APB check runsBaseProtocolChecker cannot be instantiated — you can't new BaseProtocolChecker(). But you can hold a BaseProtocolChecker handle and point it at any concrete subclass. This is how you build plug-in architectures: swap one checker for another without changing the scoreboard.
Covariant Return Types
Derived classes can return a more specific type than the base:
class Packet;
virtual function Packet clone();
Packet p = new();
// copy fields
return p;
endfunction
endclass
class AnnotatedPacket extends Packet;
string annotation;
// Return type is more specific (covariant)
virtual function AnnotatedPacket clone();
AnnotatedPacket p = new();
p.addr = this.addr;
p.annotation = this.annotation;
return p;
endfunction
endclassPolymorphism in the UVM Factory
The UVM factory is built entirely on polymorphism. You register your type:
class my_driver extends uvm_driver;
`uvm_component_utils(my_driver) // registers with factory
endclassAnd override it for a specific test without changing any test code:
// In a specific test, substitute error-injection driver:
factory.set_type_override_by_type(
my_driver::get_type(),
error_injecting_driver::get_type()
);Every place that creates my_driver now gets error_injecting_driver instead — because the factory stores uvm_component handles and uses polymorphic create() calls. Your test harness is unchanged; only the object behind the handle changed.
Virtual vs Non-Virtual — Decision Guide
Use virtual | Don't use virtual | |
|---|---|---|
| Will subclasses override this? | ✅ | |
| Is this called through a base handle? | ✅ | |
UVM phase methods (run_phase etc.) | Always virtual | |
| Helper methods only used internally | ✅ | |
| Static utility functions | ✅ | |
Constructor (new) | Never virtual (can't be) |
General rule: When in doubt, make it virtual. The cost is negligible (one vtable lookup), and it keeps the class extensible. Non-virtual saves nothing measurable and can cause subtle dispatch bugs if a subclass tries to override.
Common Pitfalls
// PITFALL 1: overriding a non-virtual method
class Base;
function void greet(); $display("Base"); endfunction
endclass
class Child extends Base;
function void greet(); $display("Child"); endfunction // shadows, doesn't override
endclass
Base b = new Child();
b.greet(); // "Base" — NOT "Child"! Because greet() is not virtual.
// PITFALL 2: forgetting to $cast before accessing derived members
Animal a_h = new Dog("Fido");
// a_h.tricks = 5; // COMPILE ERROR — Animal doesn't have 'tricks'
Dog d;
$cast(d, a_h);
d.tricks = 5; // OK after downcast
// PITFALL 3: pure virtual method not implemented in concrete subclass
virtual class Base;
pure virtual function void required_method();
endclass
class Concrete extends Base;
// forgot to implement required_method!
endclass
// COMPILE ERROR: Concrete doesn't implement all pure virtual methods