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.
// ── 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
endmoduleWhat a Child Class Inherits
Not everything transfers automatically. The access qualifier of the parent member determines whether the child can see it.
| Status | Member type |
|---|---|
| ✓ Inherited | All public properties and methods |
| ✓ Inherited | All protected properties and methods |
| ✗ Not inherited | All local properties and methods — these stay private to the parent only |
| ✓ Inherited | All static members — child shares the same static data as parent |
| ✗ Not inherited | The 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.
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
endclassOverriding 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.
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
endmoduleMulti-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.
// 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
endmoduleType 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().
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 accessibleSystemVerilog 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:
| Situation | Solution |
|---|---|
| Need data and behaviour from two base classes | Choose one as the true parent via extends. Hold the other as a property (composition). |
| Need to share a common interface | Define an abstract base class with pure virtual methods — all subclasses then share the same contract. |
| Need reusable utility methods | Put 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.
// ═══════════════════════════════════════════════════════════════
// 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
endmoduleQuick Reference
| Operation | Syntax / Rule |
|---|---|
| Declare a child class | class Child extends Parent; |
| Call parent constructor | super.new(args); — must be first line of child constructor |
| Override a method | Redeclare with same name; mark parent method virtual for dynamic dispatch |
| Call parent version inside override | super.method_name(); |
| Store child in parent handle | Always legal — child is-a parent |
| Recover child from parent handle | if (!$cast(child_h, parent_h)) ... |
| Multiple inheritance | Not 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.
<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.
<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.
<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
<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
<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.
"My override never runs through the parent handle"
DEBUGA 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.
<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>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.
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.
"My child constructor compiles but the parent's fields are uninitialised"
DEBUGA WriteTxn object has kind equal to the empty string instead of "write", despite the parent constructor setting it.
<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>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.
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.
"NULL handle dereference after $cast"
DEBUGSimulator dies with "NULL pointer dereference at scoreboard.sv:147"; that line is wt.wdata.
<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>The transaction t arrived as a ReadTxn (not a WriteTxn), so $cast left wt as null. The next line dereferenced null.
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.
"Parent's super.display() prints garbage in extended child"
DEBUGBurstWriteTxn::display() calls super.display() first, then its own additions — but the super output shows uninitialised wdata.
<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>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.
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.
"Local fields appear inherited but cannot be accessed"
DEBUGThe child class compiles fine, but the simulator reports "cannot access local member 'crc_seed' from class WriteTxn" when the child tries to read it.
<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>local means strictly private to the declaring class — children cannot see it. The user intended protected (visible to children but not outsiders).
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
- Declare every overridable method
virtualat the parent level. The cost is zero; forgetting it silently breaks polymorphism through parent handles. - 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. - 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.
- 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.
- 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.
- Always check the return value of
$cast. Wrap every cast in anif; on failure, log$typenameof the actual object before returning. - When overriding
display,copy,compare,convert2string— callsuper.method()first, then add the child's contribution. This guarantees no field is silently dropped at any inheritance level. - Use
protected(notlocal) for fields children legitimately need.localmeans strictly private to the declaring class — even direct children cannot see it. - 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.
- 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.