Module 1: Polymorphism and the Virtual Dispatch Model
Method Overriding and Virtual Methods in SystemVerilog
The Problem That Overriding Solves
When a derived class inherits a method from a base class, sometimes the base implementation is wrong or incomplete for the derived type. Overriding lets the derived class replace the base implementation with its own — keeping the same method name (so callers don't change) but providing different behavior.
Analogy: A generic Animal class has a speak() method that prints "...". A Dog extends Animal and overrides speak() to print "Woof!". Every Dog object still has a speak() method (inherited interface), but it does something dog-specific (overridden implementation). The caller just calls speak() — they don't need to know or care which type they're dealing with.
This is the foundation of open/closed design: classes are open for extension (you can add new behavior by subclassing) but closed for modification (you don't change the base class or the callers).
Non-Virtual Overriding — Static Dispatch
By default, method resolution in SV is static — determined at compile time from the declared type of the handle, regardless of what object it actually points to:
class Checker;
function void check(logic [31:0] data);
$display("Base check: data=%0h", data);
endfunction
endclass
class StrictChecker extends Checker;
function void check(logic [31:0] data); // same name — shadows, doesn't override
$display("Strict check: data=%0h, non-zero=%0b", data, |data);
endfunction
endclassStrictChecker sc = new();
Checker c = sc; // base handle points to derived object
sc.check(32'hFF); // "Strict check: ..." — handle is StrictChecker, correct
c.check(32'hFF); // "Base check: ..." — STATIC dispatch: handle is Checker → Checker's version
// Even though c POINTS to a StrictChecker object, it calls Checker::check
// because check() is NOT virtual — resolved from the declared type at compile timeWhy this is usually a bug: You created a StrictChecker specifically to get strict checking. But when it's stored in a Checker handle (as it often is in a polymorphic system), you get the base behavior, not the derived one. This silently defeats the purpose of subclassing.
virtual — Enabling Dynamic Dispatch
Add virtual to the base class method declaration, and dispatch becomes dynamic — resolved at runtime from the actual object type, not the handle's declared type:
class Checker;
virtual function void check(logic [31:0] data); // ← virtual
$display("Base check: data=%0h", data);
endfunction
endclass
class StrictChecker extends Checker;
virtual function void check(logic [31:0] data); // overrides virtual
$display("Strict check: data=%0h, non-zero=%0b", data, |data);
endfunction
endclassStrictChecker sc = new();
Checker c = sc;
sc.check(32'hFF); // "Strict check: ..." — declared type = StrictChecker ✓
c.check(32'hFF); // "Strict check: ..." — DYNAMIC dispatch: runtime type = StrictChecker ✓
// Now c behaves like a StrictChecker because that's what it IS at runtimeThe rule: If the base class method is virtual, the call is dispatched to the most-derived override in the actual object's class. The declared type of the handle is irrelevant.
The Override Chain
When a method is virtual, the entire override chain participates in dispatch — always the most-derived implementation runs:
class A;
virtual function void greet();
$display("Hello from A");
endfunction
endclass
class B extends A;
virtual function void greet();
$display("Hello from B");
endfunction
endclass
class C extends B;
virtual function void greet();
$display("Hello from C");
endfunction
endclassA handle;
handle = new A(); handle.greet(); // "Hello from A"
handle = new B(); handle.greet(); // "Hello from B" — B's override runs
handle = new C(); handle.greet(); // "Hello from C" — C's override runs
// Same handle variable, different behavior — that's polymorphismCalling super Within an Override
An override can call the parent's version via super before or after adding its own behavior:
class Transaction;
logic [31:0] addr;
int id;
virtual function void print();
$display("[Txn #%0d] addr=%0h", id, addr);
endfunction
virtual function bit is_valid();
return (addr[1:0] == 2'b00); // base rule: must be 4-byte aligned
endfunction
endclass
class WriteTransaction extends Transaction;
logic [31:0] data;
logic [3:0] strobe;
virtual function void print();
super.print(); // base info first: [Txn #N] addr=...
$display(" data=%0h strobe=%0b", data, strobe); // then extra
endfunction
virtual function bit is_valid();
if (!super.is_valid()) return 0; // check base rule first
return (strobe != 4'h0); // add: strobe must be non-zero
endfunction
endclass
class BurstWriteTransaction extends WriteTransaction;
int burst_len;
logic [31:0] data_q[$];
virtual function void print();
super.print(); // WriteTransaction.print → Transaction.print → adds data/strobe
$display(" burst_len=%0d", burst_len); // adds burst info
endfunction
virtual function bit is_valid();
if (!super.is_valid()) return 0; // check WriteTransaction rules
return (burst_len inside {[1:256]}); // add: burst len range
endfunction
endclassBurstWriteTransaction bt = new();
bt.addr = 32'h1000;
bt.data = 32'hABCD;
bt.strobe = 4'hF;
bt.burst_len = 16;
Transaction h = bt; // base handle
h.print(); // calls BurstWriteTransaction::print (most derived)
// which calls super → WriteTransaction::print → super → Transaction::print
// Output:
// [Txn #0] addr=00001000
// data=0000abcd strobe=1111
// burst_len=16
h.is_valid(); // calls BurstWriteTransaction::is_valid, which chains through all levelsThis chain of super calls is the SV equivalent of template method pattern — each level adds its own checks, building on the parent's.
virtual in Tasks
Tasks can also be virtual — enabling polymorphic time-consuming behavior:
class BaseDriver;
virtual axi_if vif;
virtual task drive(logic [31:0] addr, data);
// Default: simple single-beat drive
@(posedge vif.clk);
vif.awaddr <= addr;
vif.wdata <= data;
vif.awvalid <= 1;
@(posedge vif.clk iff vif.awready);
vif.awvalid <= 0;
endtask
endclass
class BurstDriver extends BaseDriver;
virtual task drive(logic [31:0] addr, data);
// Override: drive as burst
@(posedge vif.clk);
vif.awaddr <= addr;
vif.awlen <= 8'h7; // 8-beat burst
vif.awvalid <= 1;
@(posedge vif.clk iff vif.awready);
vif.awvalid <= 0;
repeat(8) begin
@(posedge vif.clk);
vif.wvalid <= 1;
vif.wdata <= data;
@(posedge vif.clk iff vif.wready);
end
endtask
endclassBaseDriver drv;
drv = new BurstDriver(); // set to burst driver
drv.drive(32'h1000, 32'hFF); // calls BurstDriver::drive — burst behaviorUVM relies on this heavily — run_phase is a virtual task in uvm_component. Every component overrides it with its specific behavior.
Rules for Overriding
To correctly override a virtual method:
- Same name — must match exactly
- Same argument list — number, types, and directions must match (with minor covariance exceptions)
- Same return type (or covariant for class return types)
- Re-declare
virtualin the override (not required by spec, but strongly recommended for clarity) - Cannot change to/from static — a virtual method stays virtual throughout the hierarchy
class Base;
virtual function int compute(int a, int b);
return a + b;
endfunction
endclass
class Derived extends Base;
// CORRECT override:
virtual function int compute(int a, int b); // same signature
return a * b; // different implementation
endfunction
// WRONG — different argument list (shadow, not override):
// virtual function int compute(int a); endfunction // compile error or shadow
endclassvirtual vs Non-Virtual — Decision Guide
| Scenario | Use virtual? | Reason |
|---|---|---|
| Method will be called through base class handle | ✅ Always | Without virtual, wrong version runs |
Method is a UVM phase (run_phase, build_phase) | ✅ Always | UVM framework calls via base handles |
| Internal helper method, not exposed outside class | ❌ Optional | No polymorphic use — but virtual costs nothing |
Constructor (new) | ❌ Never | Constructors cannot be virtual in SV |
| Method in a class with no subclasses (leaf class) | Optional | No practical difference, but virtual = extensible |
General rule: When in doubt, make it virtual. The overhead is a single vtable lookup — negligible in simulation. The benefit is correct polymorphic behavior if the class ever gets subclassed.
Complete Polymorphic Checker Example
class ProtocolChecker;
string name;
int error_count = 0;
function new(string name); this.name = name; endfunction
virtual function void check_addr(logic [31:0] addr);
// Base: no check (override with protocol-specific rules)
endfunction
virtual function void check_data(logic [31:0] data);
if (^data === 1'bX)
report_error($sformatf("Data contains X: %0h", data));
endfunction
function void report_error(string msg);
error_count++;
$error("[%s] %s", name, msg);
endfunction
function void summary();
$display("[%s] %0d errors", name, error_count);
endfunction
endclass
class AXI4LiteChecker extends ProtocolChecker;
function new(); super.new("AXI4L"); endfunction
virtual function void check_addr(logic [31:0] addr);
if (addr[1:0] != 2'b00)
report_error($sformatf("Unaligned addr %0h", addr));
endfunction
endclass
class APBChecker extends ProtocolChecker;
logic [31:0] max_addr;
function new(logic [31:0] max); super.new("APB"); max_addr = max; endfunction
virtual function void check_addr(logic [31:0] addr);
if (addr > max_addr)
report_error($sformatf("Addr %0h exceeds APB range", addr));
endfunction
endclass
// Polymorphic scoreboard — works with any checker
ProtocolChecker checkers[3];
checkers[0] = new AXI4LiteChecker();
checkers[1] = new APBChecker(32'hFFFF);
checkers[2] = new AXI4LiteChecker();
foreach (checkers[i]) begin
checkers[i].check_addr(32'h1001); // AXI4L catches misalignment, APB passes
checkers[i].check_data(32'hXXXX); // all catch X via base class
endCommon Pitfalls
// PITFALL 1: Non-virtual method — wrong dispatch through base handle
class Base;
function void process(); $display("Base"); endfunction // NOT virtual
endclass
class Child extends Base;
function void process(); $display("Child"); endfunction
endclass
Base h = new Child();
h.process(); // "Base" — WRONG. Add virtual to Base::process.
// PITFALL 2: Forgetting super.method() in override
virtual function bit is_valid();
// Missing: if (!super.is_valid()) return 0;
return (strobe != 0); // skips base class checks silently
endfunction
// PITFALL 3: Changing method signature in override
class Base;
virtual function void show(int x); endfunction
endclass
class Child extends Base;
virtual function void show(int x, int y); endfunction // different sig — SHADOW not override
endclass
// PITFALL 4: Making constructor virtual (not allowed)
class Foo;
virtual function new(); // COMPILE ERROR — new cannot be virtual
endfunction
endclass