Skip to content

SystemVerilog · Module 9

Inheritance & extends

Building child classes on top of existing ones, method override mechanics, multi-level hierarchies, type compatibility, and why SystemVerilog forbids multiple inheritance.

Module 9 · Page 9.8

What Inheritance Actually Does

You have a base transaction class with 10 properties and 3 methods. Now you need a write transaction — it is exactly the same, except it also needs a byte_en field and a wdata field.

Without inheritance, you copy the entire base class and paste it. Now you have two copies of the same 10 properties. When the base class changes — and it will — you update one copy and forget the other. Bugs follow.

With inheritance, the child class says extends BaseTxn and it automatically gets everything the parent has. You only write the two new fields. When the parent changes, every child benefits automatically.

extends — Basic Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Parent (base) class ───────────────────────────────────────
class BaseTxn;
    rand bit [31:0] addr;
    int               txn_id;
    string            kind;
 
    function new(string k = "base");
        kind = k;
    endfunction
 
    function void display();
        $display("[%s #%0d] addr=0x%08h", kind, txn_id, addr);
    endfunction
endclass
 
// ── Child class — inherits EVERYTHING from BaseTxn ────────────
class WriteTxn extends BaseTxn;
    //             ^^^^^^^ keyword  ^^^^^^^ parent class
 
    // New properties added by this child only
    rand bit [31:0] wdata;
    rand bit [3:0]  byte_en;
 
    function new();
        super.new("write");   // call parent constructor first
    endfunction
 
endclass
 
module tb;
    WriteTxn wt = new();
    initial begin
        void'(wt.randomize());
 
        // Inherited from BaseTxn — no redeclaration needed
        wt.txn_id = 1;
 
        // Own property
        wt.byte_en = 4'hF;
 
        // Inherited method — works on child object
        wt.display();   // [write #1] addr=0x...
    end
endmodule

What a Child Class Inherits

Not everything transfers automatically. The access qualifier of the parent member determines whether the child can see it.

StatusMember type
✓ InheritedAll public properties and methods
✓ InheritedAll protected properties and methods
✗ Not inheritedAll local properties and methods — these stay private to the parent only
✓ InheritedAll static members — child shares the same static data as parent
✗ Not inheritedThe parent's constructor — child must define its own and call super.new()

Adding New Properties and Methods

A child class is free to add any number of new properties and methods on top of what it inherited. The parent does not know about them and is not affected by them.

Adding Properties and Methods in Child
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ReadTxn extends BaseTxn;
 
    // New property — what we expect back from the DUT
    bit [31:0] expected_data;
    bit [31:0] actual_data;   // filled after drive
 
    function new();
        super.new("read");
    endfunction
 
    // New method — only ReadTxn knows how to check its response
    function bit check();
        if (actual_data !== expected_data) begin
            $error("[ReadTxn #%0d] FAIL: exp=0x%08h got=0x%08h",
                    txn_id, expected_data, actual_data);
            return 0;
        end
        $display("[ReadTxn #%0d] PASS", txn_id);
        return 1;
    endfunction
 
    // Extended display — calls parent's version, adds own info
    function void display();
        super.display();   // print base fields first
        $display("         exp=0x%08h  got=0x%08h",
                  expected_data, actual_data);
    endfunction
 
endclass

Overriding Methods

A child class can replace a parent method with its own version. This is called method overriding. Write a method with exactly the same name and signature in the child — the child's version will run when the method is called on a child object.

If you call the method through a child-class handle, the child's version runs — even without the virtual keyword. But if you call it through a parent-class handle that happens to hold a child object, you need virtual for dynamic dispatch. That is covered in full on Page 9.10 — Polymorphism. For now focus on the mechanics of overriding.

Method Override — Child Replaces Parent Version
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class BaseTxn;
    rand bit [31:0] addr;
    int               txn_id;
    string            kind;
 
    function new(string k = "base"); kind = k; endfunction
 
    virtual function void display();   // 'virtual' enables override
        $display("[%s #%0d] addr=0x%08h", kind, txn_id, addr);
    endfunction
endclass
 
class WriteTxn extends BaseTxn;
    rand bit [31:0] wdata;
    rand bit [3:0]  byte_en;
 
    function new(); super.new("write"); endfunction
 
    // Override display — completely replaces parent version
    virtual function void display();
        $display("[WR #%0d] addr=0x%08h  data=0x%08h  be=%04b",
                  txn_id, addr, wdata, byte_en);
    endfunction
endclass
 
class ErrorTxn extends BaseTxn;
    rand bit [1:0] error_type;  // 0=none 1=crc 2=timeout 3=parity
 
    function new(); super.new("error"); endfunction
 
    // Override: extend parent display, then add own info
    virtual function void display();
        super.display();   // reuse parent version for common fields
        $display("         error_type=%0d", error_type);
    endfunction
endclass
 
module tb;
    initial begin
        WriteTxn wt = new(); void'(wt.randomize()); wt.txn_id = 1;
        ErrorTxn et = new(); void'(et.randomize()); et.txn_id = 2;
 
        wt.display();
        // [WR #1] addr=0x...  data=0x...  be=1111
 
        et.display();
        // [error #2] addr=0x...
        // error_type=2
    end
endmodule

Multi-Level Inheritance

A child class can itself be extended. There is no depth limit. In practice, verification hierarchies rarely go deeper than two or three levels, but the mechanism is the same at every level.

Multi-Level — Grandchild Inherits from Child
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Level 1 — Base
class BaseTxn;
    rand bit [31:0] addr;
    int               txn_id;
    function new(); txn_id = 0; endfunction
    virtual function void display();
        $display("addr=0x%08h", addr);
    endfunction
endclass
 
// Level 2 — Child adds write fields
class WriteTxn extends BaseTxn;
    rand bit [31:0] wdata;
    function new(); super.new(); endfunction
    virtual function void display();
        super.display();
        $display("  wdata=0x%08h", wdata);
    endfunction
endclass
 
// Level 3 — Grandchild adds burst-specific fields
class BurstWriteTxn extends WriteTxn;
    rand bit [7:0]  burst_len;    // number of beats
    rand bit [1:0]  burst_type;   // FIXED / INCR / WRAP
 
    function new(); super.new(); endfunction
 
    virtual function void display();
        super.display();   // prints addr + wdata
        $display("  burst: len=%0d  type=%02b",
                  burst_len, burst_type);
    endfunction
endclass
 
module tb;
    BurstWriteTxn bt = new();
    initial begin
        void'(bt.randomize());
        bt.txn_id = 1;
        bt.display();
        // Prints all three levels of info:
        // addr=0x...      ← from BaseTxn
        // wdata=0x...     ← from WriteTxn
        // burst: len=...  ← from BurstWriteTxn
    end
endmodule

Type Compatibility — Child in a Parent Handle

A child object can always be stored in a parent-class handle. The rule is: a child IS a parent. A WriteTxn is always a BaseTxn — it has everything the parent has and more. So storing it in a BaseTxn handle is safe.

The reverse is not automatically true. A BaseTxn handle might hold a WriteTxn or might hold a plain BaseTxn. The compiler does not know at compile time. To go from parent to child handle you must use $cast().

Type Compatibility and $cast
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BaseTxn  base;
WriteTxn wt;
 
// ── Child → Parent: always legal (upcasting) ─────────────────
wt   = new();
base = wt;     // WriteTxn stored in BaseTxn handle — fine
               // base.addr, base.txn_id etc are accessible
               // base.wdata is NOT accessible through 'base' handle
               // (wdata is hidden but the object still has it)
 
// ── Parent → Child: needs $cast (downcasting) ─────────────────
BaseTxn  b2 = new();   // plain BaseTxn object
WriteTxn w2;
 
// $cast returns 1 on success, 0 on failure
if (!$cast(w2, b2))
    $display("Cast failed — b2 is not a WriteTxn");   // prints this
 
// Now try with an actual WriteTxn stored in a BaseTxn handle
BaseTxn  b3 = new WriteTxn();   // WriteTxn in BaseTxn handle
WriteTxn w3;
 
if ($cast(w3, b3))
    $display("Cast OK — w3 is now a WriteTxn handle");
    // Now w3.wdata is accessible

SystemVerilog Has No Multiple Inheritance

SystemVerilog allows a class to extend exactly one parent class — no more. There is no class C extends A, B;. If you need behaviour from two sources, the common patterns are:

SituationSolution
Need data and behaviour from two base classesChoose one as the true parent via extends. Hold the other as a property (composition).
Need to share a common interfaceDefine an abstract base class with pure virtual methods — all subclasses then share the same contract.
Need reusable utility methodsPut them in a package and import. Package functions are not inherited but are accessible everywhere they are imported.

Full Verification Example — APB Transaction Family

A complete, runnable transaction hierarchy covering the three types you will write on almost every protocol verification project.

Full Example — APB Base + Write + Read + Error Transactions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ═══════════════════════════════════════════════════════════════
// Level 1 — Base transaction: common fields for all APB txns
// ═══════════════════════════════════════════════════════════════
class ApbBaseTxn;
 
    static int  next_id    = 1;
    rand bit [31:0] addr;
    int                txn_id;
    bit [1:0]          resp;    // 00=OKAY 10=SLVERR
 
    constraint c_align { addr[1:0] == 2'b00; }
 
    function new();
        txn_id = next_id++;
        resp   = 2'b00;
    endfunction
 
    virtual function void display();
        $display("[APB #%0d] addr=0x%08h  resp=%02b",
                  txn_id, addr, resp);
    endfunction
 
    function bit is_okay();
        return (resp == 2'b00);
    endfunction
 
endclass
 
// ═══════════════════════════════════════════════════════════════
// Level 2a — Write transaction
// ═══════════════════════════════════════════════════════════════
class ApbWriteTxn extends ApbBaseTxn;
 
    rand bit [31:0] pwdata;
    rand bit [3:0]  pstrb;    // byte strobes
 
    function new(); super.new(); endfunction
 
    virtual function void display();
        super.display();
        $display("         WR  data=0x%08h  strb=%04b", pwdata, pstrb);
    endfunction
 
endclass
 
// ═══════════════════════════════════════════════════════════════
// Level 2b — Read transaction
// ═══════════════════════════════════════════════════════════════
class ApbReadTxn extends ApbBaseTxn;
 
    bit [31:0] prdata;         // filled after drive
    bit [31:0] expected_data;
 
    function new(bit [31:0] exp = 32'h0);
        super.new();
        expected_data = exp;
    endfunction
 
    virtual function void display();
        super.display();
        $display("         RD  prdata=0x%08h  exp=0x%08h  %s",
                  prdata, expected_data,
                  (prdata === expected_data) ? "MATCH" : "MISMATCH");
    endfunction
 
    function bit check();
        return (prdata === expected_data);
    endfunction
 
endclass
 
// ═══════════════════════════════════════════════════════════════
// Level 2c — Error injection transaction
// ═══════════════════════════════════════════════════════════════
class ApbErrorTxn extends ApbBaseTxn;
 
    typedef enum {ERR_NONE,ERR_SLVERR,ERR_TIMEOUT} err_e;
    rand err_e error_kind;
 
    function new(); super.new(); endfunction
 
    function void apply_error();
        case (error_kind)
            ERR_SLVERR  : resp = 2'b10;
            default     : resp = 2'b00;
        endcase
    endfunction
 
    virtual function void display();
        super.display();
        $display("         ERR kind=%s", error_kind.name());
    endfunction
 
endclass
 
// ═══════════════════════════════════════════════════════════════
// Testbench
// ═══════════════════════════════════════════════════════════════
module tb;
    initial begin
 
        ApbWriteTxn wr = new();
        ApbReadTxn  rd = new(32'hFF00_FF00);
        ApbErrorTxn er = new();
 
        void'(wr.randomize());
        void'(rd.randomize());
        void'(er.randomize());
 
        er.apply_error();
 
        wr.display();
        rd.prdata = 32'hFF00_FF00;  // simulate DUT response
        rd.display();
        er.display();
 
        $display("\nRead check: %0b", rd.check());  // 1 (match)
        $display("Error okay: %0b", er.is_okay()); // depends on random
 
    end
endmodule

Quick Reference

OperationSyntax / Rule
Declare a child classclass Child extends Parent;
Call parent constructorsuper.new(args); — must be first line of child constructor
Override a methodRedeclare with same name; mark parent method virtual for dynamic dispatch
Call parent version inside overridesuper.method_name();
Store child in parent handleAlways legal — child is-a parent
Recover child from parent handleif (!$cast(child_h, parent_h)) ...
Multiple inheritanceNot supported — use composition or abstract base class instead

Verification Usage — How Inheritance Shapes a Real Testbench

Inheritance is the spine of every modern UVM environment. Three concrete patterns dominate production code; understanding them is the difference between writing throwaway tests and writing reusable verification IP.

Transaction class family (the canonical case)

Every protocol VIP starts with a common base_transaction carrying the universal fields — address, ID, timestamp, error flag — and grows a child per operation type.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class apb_base_xact extends uvm_sequence_item;
  rand bit [31:0] paddr;
  rand bit        psel;
        bit        pready_observed;
  int             xact_id;
  // common randomize(), copy(), compare(), convert2string()
endclass
 
class apb_write_xact extends apb_base_xact;
  rand bit [31:0] pwdata;
  rand bit [3:0]  pstrb;
endclass
 
class apb_read_xact extends apb_base_xact;
       bit [31:0] prdata;     // sampled, not randomised
endclass</code>

Component class family (uvm_component hierarchy)

Every UVM testbench class — driver, monitor, scoreboard, sequencer — ultimately extends uvm_component. Each level adds one specific responsibility: uvm_object → uvm_report_object → uvm_component → uvm_driver → my_apb_driver. You almost never touch the upper layers, but you live inside the lower ones.

Sequence class family (test scenarios)

A base sequence handles boilerplate (start_item / finish_item, get_response, error injection hooks). Each test-specific sequence extends it and overrides just the body() task with the scenario logic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class base_seq extends uvm_sequence #(apb_base_xact);
  task pre_body();  // raise objection, setup
  task post_body(); // drop objection, teardown
endclass
 
class smoke_seq extends base_seq;
  task body();
    apb_write_xact w;
    `uvm_do_with(w, { paddr inside {32'h0000_0000, 32'h0000_0004}; })
  endtask
endclass</code>

Simulation Behavior — What the Simulator Actually Builds

Inheritance is not a runtime concept — it is fully resolved at elaboration. Understanding the static structure the simulator builds makes the runtime behaviour intuitive.

Object memory layout

A child object is laid out as parent fields first, then child fields, in declaration order. There is exactly one contiguous block of memory per object — not a parent object plus a child object linked together. This is why upcasting (child → parent handle) is free: the parent's view simply ignores the trailing child-only fields.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>BaseTxn  layout:  [ addr | txn_id | kind ]
WriteTxn layout:  [ addr | txn_id | kind | wdata | byte_en ]
                  └── BaseTxn view ───┘
                  └────── WriteTxn view ──────────────────┘</code>

Virtual method dispatch table

Each class with at least one virtual method gets a dispatch table (vtable) — an array of function pointers, one per virtual method. Every object carries a hidden pointer to its class's vtable. A call like handle.display() compiles to: follow the vtable pointer → look up display's slot → call the function at that slot. The handle's declared type is irrelevant at runtime; the object's actual class is what matters.

Constructor chaining order

When you construct a child, the parent's new() runs first (via the implicit or explicit super.new() call), then the child's body. The order is top-down for initialization, bottom-up for destruction — same as C++ and Java. There is no destructor in SystemVerilog, but the order matters for static fan-out (e.g., a child that registers itself with a static collection inherited from the parent).

Waveform Analysis — Tracking Inherited Transactions in the Dump

Inherited class hierarchies are invisible in the waveform viewer — there's no signal that says "this transaction is a WriteTxn." The cure is to surface a small per-transaction metadata signal that captures the dynamic type.

Encoding transaction type as a numeric signal

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>// Inside the testbench module:
typedef enum bit [2:0] {
  T_NONE = 0, T_WRITE = 1, T_READ = 2, T_ERROR = 3, T_BURST_WR = 4
} txn_type_e;
 
txn_type_e wave_last_type;
int        wave_last_id;
 
function void log_txn(BaseTxn t);
  WriteTxn      wt;  ReadTxn rt;  ErrorTxn et;  BurstWriteTxn bwt;
  if      ($cast(bwt, t)) wave_last_type = T_BURST_WR;
  else if ($cast(wt,  t)) wave_last_type = T_WRITE;
  else if ($cast(rt,  t)) wave_last_type = T_READ;
  else if ($cast(et,  t)) wave_last_type = T_ERROR;
  else                    wave_last_type = T_NONE;
  wave_last_id = t.txn_id;
endfunction</code>

ASCII waveform — a mixed transaction stream

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>           0ns       200ns      400ns      600ns      800ns
clk        _|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_
last_type  ==WRITE====READ====WRITE====BURST_WR========ERROR=
last_id    ====1=======2=======3=========4===============5===
                                         ^
                                         |
                              4-beat burst write starts here</code>

Industry Insights — Wisdom From Production UVM Codebases

Debugging Academy — Five Inheritance Bugs You Will Meet

Each lab is drawn from a real bring-up session. Study the symptom and the buggy code; the fix appears at the bottom of each block.

1

"My override never runs through the parent handle"

DEBUG
Symptom

A child display() works when called through a child handle but produces the parent's output when called through a BaseTxn handle holding the same child object.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class BaseTxn;
  function void display();          // BUG: not virtual
    $display("[base] addr=0x%h", addr);
  endfunction
endclass
 
class WriteTxn extends BaseTxn;
  function void display();          // intended override
    $display("[WR] addr=0x%h data=0x%h", addr, wdata);
  endfunction
endclass
 
BaseTxn h = WriteTxn::new();
h.display();   // prints "[base] addr=..." — NOT the WriteTxn version!</code>
Root Cause

Without virtual on the parent method, the call h.display() is resolved at compile time using the handle's declared type (BaseTxn), not the object's runtime type. The override is invisible.

Fix

Add virtual to the parent declaration: virtual function void display();. This forces dynamic dispatch via the vtable. The child does not need virtual again, but adding it for symmetry is a common convention.

2

"My child constructor compiles but the parent's fields are uninitialised"

DEBUG
Symptom

A WriteTxn object has kind equal to the empty string instead of "write", despite the parent constructor setting it.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class BaseTxn;
  string kind;
  function new(string k = "base"); kind = k; endfunction
endclass
 
class WriteTxn extends BaseTxn;
  bit [31:0] wdata;
  function new();                 // BUG: no super.new() call
    wdata = 0;
  endfunction
endclass</code>
Root Cause

If the parent constructor takes arguments with defaults, the simulator inserts an implicit super.new() with defaults — so kind becomes "base", not "write". The user expected "write" and didn't realise the implicit call used the default.

Fix

Always make super.new() explicit and pass the right arguments: function new(); super.new("write"); wdata = 0; endfunction. Explicit is always better than implicit when constructor arguments matter.

3

"NULL handle dereference after $cast"

DEBUG
Symptom

Simulator dies with "NULL pointer dereference at scoreboard.sv:147"; that line is wt.wdata.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>function void scoreboard::write(BaseTxn t);
  WriteTxn wt;
  $cast(wt, t);                       // BUG: return value ignored
  $display("data=0x%h", wt.wdata);   // crashes if cast failed
endfunction</code>
Root Cause

The transaction t arrived as a ReadTxn (not a WriteTxn), so $cast left wt as null. The next line dereferenced null.

Fix

Always wrap $cast in an if: if (!$cast(wt, t)) begin uvm_error("SB", $sformatf("expected WriteTxn, got %s", $typename(t))); return; end`. The fail branch should both log the actual type and bail out before any downstream use.

4

"Parent's super.display() prints garbage in extended child"

DEBUG
Symptom

BurstWriteTxn::display() calls super.display() first, then its own additions — but the super output shows uninitialised wdata.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class WriteTxn extends BaseTxn;
  rand bit [31:0] wdata;
  function new(); super.new("write"); endfunction
  virtual function void display();
    super.display();
    $display("  data=0x%h", wdata);
  endfunction
endclass
 
class BurstWriteTxn extends WriteTxn;
  rand int len;
  function new(); super.new(); endfunction
  // BUG: forgot to override display, and burst code calls
  //      bwt.randomize() but never calls display itself
endclass
 
BurstWriteTxn bwt = new();
assert(bwt.randomize());
bwt.display();    // prints stale wdata from before randomize()?</code>
Root Cause

Actually the wdata is fine — what's wrong is that the user expected to see burst-specific fields. Because BurstWriteTxn never overrode display, only the inherited WriteTxn::display ran, omitting len.

Fix

Every leaf class with new fields must override display() (and convert2string(), and compare()) and call super first: virtual function void display(); super.display(); $display(" burst len=%0d", len); endfunction. The base method handles common fields; each level adds its own.

5

"Local fields appear inherited but cannot be accessed"

DEBUG
Symptom

The child class compiles fine, but the simulator reports "cannot access local member 'crc_seed' from class WriteTxn" when the child tries to read it.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class BaseTxn;
  local int crc_seed = 32'hDEAD_BEEF;     // local = invisible to children
 
  protected function int compute_crc();
    return crc_seed ^ addr;
  endfunction
endclass
 
class WriteTxn extends BaseTxn;
  function void show_seed();
    $display("seed=0x%h", crc_seed);      // ILLEGAL: local stays in BaseTxn
  endfunction
endclass</code>
Root Cause

local means strictly private to the declaring class — children cannot see it. The user intended protected (visible to children but not outsiders).

Fix

Either (a) change local to protected if children genuinely need access, or (b) keep local and provide a protected getter: protected function int get_crc_seed(); return crc_seed; endfunction. Option (b) preserves encapsulation more strictly.

Interview Q&A — Twelve Questions You Will Be Asked

It declares a new class as a child of an existing class. The child inherits all public, protected, and static members of the parent — properties and methods — and is free to add new ones and override existing virtual ones. Syntax: class Child extends Parent;.

Best Practices — Ten Rules for Disciplined Inheritance

  1. Declare every overridable method virtual at the parent level. The cost is zero; forgetting it silently breaks polymorphism through parent handles.
  2. Always make super.new(args) explicit as the first line of the child constructor. Implicit chaining uses parent defaults, which is rarely what you want.
  3. Keep inheritance trees shallow — 2 to 3 levels is the sweet spot. Deeper trees increase cognitive load without proportional benefit. Reach for composition past three levels.
  4. Put only universal fields and behaviour in the base class. Anything that is meaningful to fewer than all children belongs in a child or a mixin, not the base.
  5. Prefer composition (has-a) over inheritance (is-a) whenever the relationship is not a strict substitution. A scoreboard has-a FIFO; it is not a kind of FIFO.
  6. Always check the return value of $cast. Wrap every cast in an if; on failure, log $typename of the actual object before returning.
  7. When overriding display, copy, compare, convert2string — call super.method() first, then add the child's contribution. This guarantees no field is silently dropped at any inheritance level.
  8. Use protected (not local) for fields children legitimately need. local means strictly private to the declaring class — even direct children cannot see it.
  9. Never store handle-type-specific logic in the base class. If the base needs to know which child it's serving, the design has the inheritance arrow reversed.
  10. Document the inheritance contract at the top of every base class. What must children override? What must children NOT override? Explicit contracts prevent silent breakages months later.