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
endclass
StrictChecker 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 time

Why 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
endclass
StrictChecker 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 runtime

The 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
endclass
A 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 polymorphism

Calling 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
endclass
BurstWriteTransaction 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 levels

This 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
endclass
BaseDriver drv;
drv = new BurstDriver();   // set to burst driver

drv.drive(32'h1000, 32'hFF);   // calls BurstDriver::drive — burst behavior

UVM 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:

  1. Same name — must match exactly
  2. Same argument list — number, types, and directions must match (with minor covariance exceptions)
  3. Same return type (or covariant for class return types)
  4. Re-declare virtual in the override (not required by spec, but strongly recommended for clarity)
  5. 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
endclass

virtual vs Non-Virtual — Decision Guide

ScenarioUse virtual?Reason
Method will be called through base class handle✅ AlwaysWithout virtual, wrong version runs
Method is a UVM phase (run_phase, build_phase)✅ AlwaysUVM framework calls via base handles
Internal helper method, not exposed outside class❌ OptionalNo polymorphic use — but virtual costs nothing
Constructor (new)❌ NeverConstructors cannot be virtual in SV
Method in a class with no subclasses (leaf class)OptionalNo 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
end

Common 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