SystemVerilog · Module 9
typedef class — Forward Declarations
Breaking circular references between mutually-referencing classes, request/response pairs, callback patterns, and the orphan-forward trap that surfaces as cryptic 'incomplete type' errors.
Module 9 · Page 9.15
The Problem — Circular Class References
Imagine you have two classes: Driver holds a handle to a Scoreboard, and Scoreboard holds a handle back to a Driver.
Which one do you define first? If you write Driver first, the compiler hits Scoreboard inside it and says "I don't know what Scoreboard is." If you write Scoreboard first, the same problem occurs with Driver. This is a circular dependency.
Circular Dependency — Both Classes Need Each OtherDriver Scoreboard sb; ← needs Scoreboard task run(); Driver needs Scoreboard⇄Scoreboard needs DriverScoreboard Driver drv; ← needs Driver function check();
typedef class breaks this deadlock. You give the compiler a heads-up that a class with that name exists, without providing the full definition yet. The compiler accepts it and moves on.
Syntax — One Line, That Is All
// ── Step 1: Forward declaration — tells compiler the name exists ─
typedef class Scoreboard; // just a name, no body yet
// ── Step 2: Define the first class — can now use Scoreboard ─────
class Driver;
Scoreboard sb; // OK — compiler knows Scoreboard exists
function new(Scoreboard s);
sb = s;
endfunction
task run();
// sb.check() would NOT work here — full definition not yet seen
endtask
endclass
// ── Step 3: Full definition of Scoreboard ────────────────────────
class Scoreboard;
Driver drv; // OK — Driver is already fully defined above
int pass_count;
function new(Driver d);
drv = d;
pass_count = 0;
endfunction
function void check();
$display("[SB] Checking...");
endfunction
endclassWhat You Can and Cannot Do With a Forward Declaration
A forward declaration tells the compiler a name exists. It does not give the compiler any information about the class's methods or properties. So there are strict limits on what you can do before the full definition appears. ✅ Allowed after typedef class
- ✓ Declare a handle:
Scoreboard sb; - ✓ Use as a function/task argument type
- ✓ Use as a return type
- ✓ Check null:
if (sb == null)❌ NOT allowed until full definition - ✗ Create an object:
sb = new() - ✗ Call any method:
sb.check() - ✗ Access any property:
sb.pass_count - ✗ Extend it:
class X extends Scoreboard
typedef class Scoreboard;
class Driver;
Scoreboard sb; // ✅ OK — declare handle
function new(Scoreboard s); // ✅ OK — argument type
sb = s;
endfunction
task run();
// sb = new(); ← ❌ ERROR: cannot instantiate
// sb.check(); ← ❌ ERROR: method unknown
// sb.pass_count++; ← ❌ ERROR: property unknown
if (sb != null) // ✅ OK — null check
$display("sb handle is set");
endtask
endclass
// After full Scoreboard definition, all operations become legal.Verification Use Case — Request and Response Pair
A common verification pattern: a Request object holds a handle to its expected Response, and the Response holds a back-reference to the original request for traceability.
// Forward declare Response before Request uses it
typedef class Response;
class Request;
rand bit [31:0] addr;
rand bit [31:0] data;
int req_id;
Response rsp; // forward-declared handle — OK
function new(int id = 0);
req_id = id;
rsp = null;
endfunction
function void display();
$display("[REQ #%0d] addr=0x%08h data=0x%08h",
req_id, addr, data);
endfunction
endclass
class Response;
bit [1:0] status; // 00=OKAY 10=ERROR
bit [31:0] rdata;
Request orig_req; // back-reference to the request
function new(Request req = null);
orig_req = req;
status = 2'b00;
endfunction
function void display();
$display("[RSP for REQ#%0d] status=%02b rdata=0x%08h",
orig_req.req_id, status, rdata);
endfunction
endclass
module tb;
initial begin
Request req = new(1);
void'(req.randomize());
Response rsp = new(req); // response holds back-reference
rsp.rdata = 32'h1234_5678;
req.rsp = rsp; // request holds forward-reference
req.display();
rsp.display();
end
endmoduletypedef as a Type Alias — A Different Use
typedef class ClassName; is a forward declaration. But typedef can also create a type alias — a short name for an existing type. These look similar but do completely different things.
// ── 1. Forward declaration — class definition comes later ─────
typedef class Scoreboard; // "Scoreboard exists, trust me"
// ── 2. Type alias — short name for an existing type ───────────
class AhbTransaction;
rand bit [31:0] addr;
endclass
typedef AhbTransaction AhbTxn; // alias — AhbTxn == AhbTransaction
// Now use the short name everywhere
AhbTxn t = new(); // identical to AhbTransaction t = new()
// ── 3. typedef with parameterised classes ──────────────────────
class GenQueue #(type T = int);
T items[$];
endclass
typedef GenQueue #(AhbTxn) AhbQueue; // named specialisation
AhbQueue q = new();| Form | Syntax | Purpose |
|---|---|---|
| Forward declaration | typedef class Name; | Tells compiler the class exists. Full body comes later. Solves circular dependency. |
| Type alias | typedef ExistingClass ShortName; | Creates a shorter or more readable name for an already-defined class. |
| Parameterised alias | typedef GenClass #(T) SpecName; | Names a specific parameterised version for repeated use. |
Complete Working Code — Step by Step
Here is a fully self-contained, runnable example. Every line that matters is annotated inline so you can read the code and explanation together without jumping between sections. Copy it into your simulator and run it directly.
Step 1 — Understand the Dependency
Driver needs to call sb.add_expected(t) after driving each transaction — so Driver must hold a Scoreboard handle. Scoreboard needs to call drv.get_name() in its report — so Scoreboard must hold a Driver handle. Neither can be defined first without the other — circular dependency.
Step 2 — Solution: typedef class at the Top
Declare typedef class Scoreboard; before Driver. Now the compiler accepts Scoreboard as a handle type inside Driver. Define Scoreboard fully after Driver — by which point the compiler already knows both classes completely.
Step 3 — Complete Working Code
// ════════════════════════════════════════════════════════════════
// STEP 1 — Forward Declaration
// Without this one line, 'Driver' below cannot use 'Scoreboard'
// as a type because the compiler has not seen it yet.
// ════════════════════════════════════════════════════════════════
typedef class Scoreboard; // tells compiler: Scoreboard exists
// ════════════════════════════════════════════════════════════════
// STEP 2 — Transaction Class
// Simple data container. Both Driver and Scoreboard work with it.
// No circular dependency here — defined before everything else.
// ════════════════════════════════════════════════════════════════
class ApbTxn;
rand bit [31:0] addr;
rand bit [31:0] data;
int txn_id;
static int next_id = 1; // auto-increment ID
constraint c_align { addr[1:0] == 2'b00; } // word-aligned
function new();
txn_id = next_id++;
endfunction
function void display(string tag = "");
$display("%s[TXN#%0d] addr=0x%08h data=0x%08h",
tag, txn_id, addr, data);
endfunction
function ApbTxn copy();
copy = new();
copy.txn_id = txn_id;
copy.addr = addr;
copy.data = data;
endfunction
endclass
// ════════════════════════════════════════════════════════════════
// STEP 3 — Driver Class
//
// Driver needs Scoreboard → uses the forward-declared type.
// At this point the compiler only knows Scoreboard's NAME.
// So we can: ✅ declare a Scoreboard handle (sb)
// ✅ use Scoreboard as a function argument type
// But we cannot yet: ❌ call sb.add_expected(t) ← methods unknown
//
// The actual method call is placed in the task body, which is
// resolved at link time when Scoreboard is fully defined.
// ════════════════════════════════════════════════════════════════
class Driver;
Scoreboard sb; // ← forward-declared type as a handle: LEGAL
string drv_name;
int drive_cnt;
function new(string name, Scoreboard s); // ← argument type: LEGAL
drv_name = name;
sb = s;
drive_cnt = 0;
endfunction
function string get_name();
return drv_name;
endfunction
// Task body is resolved later — Scoreboard methods are usable here
task drive(ApbTxn t);
t.display("[DRV] ");
sb.add_expected(t); // ← works because Scoreboard is fully
drive_cnt++; // defined before this task is ever called
endtask
function void report();
$display("[%s] Total driven: %0d", drv_name, drive_cnt);
endfunction
endclass
// ════════════════════════════════════════════════════════════════
// STEP 4 — Scoreboard Class (full definition)
//
// By the time the compiler reaches here, Driver is fully defined.
// So Scoreboard can safely use Driver as a property type AND call
// its methods — no forward declaration needed for Driver.
// ════════════════════════════════════════════════════════════════
class Scoreboard;
Driver drv; // ← Driver fully defined above: LEGAL
ApbTxn expected_q[$]; // queue of expected transactions
ApbTxn actual_q[$]; // queue of actual DUT responses
int pass_cnt;
int fail_cnt;
function new();
pass_cnt = 0;
fail_cnt = 0;
endfunction
// Called by Driver — stores a copy of each driven transaction
function void add_expected(ApbTxn t);
expected_q.push_back(t.copy());
$display("[SB] Stored expected TXN#%0d", t.txn_id);
endfunction
// Called by monitor (simulated here) — stores DUT response
function void add_actual(ApbTxn t);
actual_q.push_back(t);
endfunction
// Wire the scoreboard to a driver — can use Driver methods here
function void set_driver(Driver d);
drv = d;
$display("[SB] Connected to driver: %s", drv.get_name());
endfunction
// Compare expected vs actual, field by field
function void check_all();
int n = (expected_q.size() < actual_q.size())
? expected_q.size() : actual_q.size();
for (int i = 0; i < n; i++) begin
if (expected_q[i].addr === actual_q[i].addr &&
expected_q[i].data === actual_q[i].data) begin
pass_cnt++;
$display("[SB] PASS TXN#%0d", expected_q[i].txn_id);
end else begin
fail_cnt++;
$display("[SB] FAIL TXN#%0d", expected_q[i].txn_id);
expected_q[i].display(" EXP: ");
actual_q[i].display(" GOT: ");
end
end
endfunction
function void report();
$display("[SB] ─── Final Report ───");
$display("[SB] Driver : %s", drv.get_name());
$display("[SB] Expected: %0d", expected_q.size());
$display("[SB] PASS : %0d", pass_cnt);
$display("[SB] FAIL : %0d", fail_cnt);
endfunction
endclass
// ════════════════════════════════════════════════════════════════
// STEP 5 — Testbench Module
//
// 1. Create Scoreboard first (Driver constructor needs it)
// 2. Create Driver, pass Scoreboard handle
// 3. Connect Scoreboard back to Driver via set_driver()
// 4. Drive transactions — each one is stored in scoreboard
// 5. Simulate DUT response — copy each expected as actual
// (in a real TB the monitor fills actual_q)
// 6. Check and report
// ════════════════════════════════════════════════════════════════
module tb;
Scoreboard sb;
Driver drv;
initial begin
// ── Build the environment ──────────────────────────────
sb = new();
drv = new("apb_drv", sb); // Driver stores sb handle
sb.set_driver(drv); // Scoreboard stores drv handle
// [SB] Connected to driver: apb_drv
// ── Drive 4 random transactions ────────────────────────
$display("\n--- Driving ---");
repeat(4) begin
ApbTxn t = new();
if (!t.randomize()) $fatal(1, "randomize failed");
drv.drive(t); // prints TXN + calls sb.add_expected(t)
end
// ── Simulate monitor: copy expected into actual ─────────
// In a real TB this is filled by the bus monitor.
// Here we copy directly to demonstrate the check logic.
$display("\n--- Monitor response ---");
foreach(sb.expected_q[i])
sb.add_actual(sb.expected_q[i].copy());
// ── Inject one deliberate mismatch in actual[2] ─────────
sb.actual_q[2].data = 32'hFFFF_FFFF;
// ── Run scoreboard checks ───────────────────────────────
$display("\n--- Checking ---");
sb.check_all();
// ── Print final report (uses drv.get_name() internally) ─
$display("");
drv.report();
sb.report();
end
endmodule
// ════════════════════════════════════════════════════════════════
// EXPECTED OUTPUT:
// [SB] Connected to driver: apb_drv
//
// --- Driving ---
// [DRV] [TXN#1] addr=0x... data=0x...
// [SB] Stored expected TXN#1
// [DRV] [TXN#2] addr=0x... data=0x...
// [SB] Stored expected TXN#2
// [DRV] [TXN#3] addr=0x... data=0x...
// [SB] Stored expected TXN#3
// [DRV] [TXN#4] addr=0x... data=0x...
// [SB] Stored expected TXN#4
//
// --- Checking ---
// [SB] PASS TXN#1
// [SB] PASS TXN#2
// [SB] FAIL TXN#3 ← deliberate mismatch on data
// EXP: [TXN#3] addr=0x... data=0x...
// GOT: [TXN#3] addr=0x... data=0xffffffff
// [SB] PASS TXN#4
//
// [apb_drv] Total driven: 4
// [SB] ─── Final Report ───
// [SB] Driver : apb_drv
// [SB] Expected: 4
// [SB] PASS : 3
// [SB] FAIL : 1
// ════════════════════════════════════════════════════════════════Quick Reference
| Operation | Syntax | Notes |
|---|---|---|
| Forward declaration | typedef class ClassName; | One line. No body. Place before the class that needs to reference it. |
| Declare handle after typedef | ClassName h; | ✅ Allowed — compiler knows the type name exists |
| Use as argument/return type | function void f(ClassName c); | ✅ Allowed |
| Create object before full definition | h = new(); | ❌ Not allowed — full class body must be visible first |
| Call method before full definition | h.method(); | ❌ Not allowed — methods unknown without full definition |
| Type alias | typedef ExistingClass ShortName; | Different use — creates a short name for an already-defined class |
Verification Usage — Where Forward Declarations Become Essential
Forward declarations are not exotic — they sit quietly inside almost every UVM environment, every callback framework, and every paired driver/responder pattern. Three recurring situations make typedef class unavoidable.
Mutually-referencing request and response transactions
A request transaction often needs to point back at its matching response (for correlation in scoreboards), while the response holds a reference to the original request (for context in error reporting). Both classes hold a handle to the other — a perfect circular reference that only typedef class can break.
<code>typedef class read_rsp; // forward — break the cycle
class read_req extends uvm_sequence_item;
bit [31:0] addr;
read_rsp matched_rsp; // handle uses only the forward decl
endclass
class read_rsp extends uvm_sequence_item;
bit [31:0] data;
read_req orig_req; // back-reference for context
endclass</code>Callback pattern — owner refers to callback, callback refers to owner
Reusable VIPs implement the callback pattern: a base callback class holds a handle to its owning component (so hooks can read owner state), and the owning component maintains a queue of callbacks (so it can fire each one at the right phase). Each class holds a handle to the other.
Cross-component scoreboard wiring
A multi-agent scoreboard often holds handles to each agent's monitor, while each monitor holds a reference back to the scoreboard for direct delivery (a faster alternative to going through analysis ports for high-throughput VIPs). Either both classes live in one file and use typedef class, or both files include a small shared header that forward-declares both.
Simulation Behavior — How the Compiler Resolves Forward Declarations
Two-pass name resolution
SystemVerilog's compiler effectively makes two passes over the source. The first pass collects type names — including those introduced by typedef class — without examining their bodies. The second pass resolves references using the now-complete symbol table. A forward declaration's only job is to make a name visible in pass one.
What the forward declaration tells the compiler
The single line typedef class scoreboard; communicates exactly: "there is a class type named scoreboard; its body will appear later in this compilation unit." That is enough to legally declare handles, pass them as arguments, return them, and store them in arrays — but nothing more, because nothing else is known about the class yet.
Forward declaration is local to its scope
A typedef class at module scope is visible throughout that module; one inside a class is visible inside that class; one inside a package is exported via import. The forward and its full definition must be in the same scope — you cannot forward-declare in one package and define in another.
Runtime is identical
Forward-declared classes generate the same code as classes declared "in order" — the simulator's runtime knows nothing about the order in which the compiler encountered names. There is no extra indirection, no runtime check, no memory footprint difference. The construct is pure compile-time scaffolding.
<code>typedef class B; // forward
class A;
B b_handle; // legal — name known
endclass
class B;
A a_handle;
endclass
// Compiles cleanly.</code><code>class A;
B b_handle; // ERROR: 'B' is unknown
endclass
class B;
A a_handle; // 'A' is fine — already seen
endclass
// Compiler rejects A because B
// has not been declared yet.</code>Waveform Analysis — Tracking Mutually-Referencing Classes in Debug
Forward-declared classes leave no special waveform footprint, but the request/response and callback patterns they enable produce log streams where the cross-reference is the most useful debug signal. Always log both sides of the reference so a reader can reconstruct the linked pair.
The trace pattern
<code>function void scoreboard::on_response(read_rsp r);
$display("[%0t] RSP id=%0d matched_to_req=%0d data=0x%08h",
$time, r.get_transaction_id(),
r.orig_req.get_transaction_id(), // back-reference!
r.data);
endfunction</code>ASCII view — request/response correlation in logs
<code>100ns REQ id=1 addr=0x4000 expecting matched_rsp
180ns RSP id=1 matched_to_req=1 data=0xDEADBEEF
200ns REQ id=2 addr=0x4004 expecting matched_rsp
280ns RSP id=2 matched_to_req=2 data=0xCAFEBABE
↑
back-reference proves the
cross-class wiring is intact</code>Industry Insights — Hard-Won Lessons About typedef class
Debugging Academy — Five Real Bugs Around typedef class
"Compile error: 'B' is an unknown type"
DEBUGTwo mutually-referencing classes refuse to compile — the first one cannot find the type name of the second.
<code>class request;
response matched_rsp; // BUG: 'response' not yet declared
endclass
class response;
request orig_req;
endclass</code>SystemVerilog requires type names to be visible at the point of reference. Without a forward declaration, response is unknown when the compiler parses request.
Add a forward declaration above both classes: typedef class response; Then both classes parse correctly, even though they reference each other.
"Compile error: cannot call method on incomplete type"
DEBUGA forward declaration parses fine, but a function trying to call a method on the forward-declared type fails with "member access on incomplete type."
<code>typedef class scoreboard;
function void log_to(scoreboard sb, int id);
sb.add_entry(id); // BUG: sb's interface not yet defined
endfunction
class scoreboard;
function void add_entry(int id); /* ... */ endfunction
endclass</code>A forward declaration introduces only the name — at the point of the function definition the compiler does not yet know scoreboard has a method called add_entry. The forward only lets you pass handles around, not call methods.
Move the function body so it appears after the full class definition. Or, declare the function as extern and define its body later: extern function void log_to(scoreboard sb, int id); at the top, then the body — after the full class — fills in the method call.
"Forward declaration silently accepted but never followed by full definition"
DEBUGCode compiles for some time, accumulating typedef class foo; + handles of type foo, until eventually someone tries to call a method on a foo and gets "type 'foo' is incomplete" in a confusing place.
<code>// Somewhere in env_pkg.sv
typedef class my_widget;
class component;
my_widget w; // legal — just a handle
endclass
// ... no full definition of my_widget ever provided ...
function void use(my_widget x);
x.do_thing(); // ERROR — but we don't know my_widget was never defined
endfunction</code>SystemVerilog allows forward declarations to remain "unfulfilled" through to link time. Code that only needs a handle continues to compile until something actually tries to use the class, at which point the cryptic "incomplete type" error appears.
Discipline — every typedef class X; should be followed in the same compilation unit by a visible class X; ... endclass. As a CI check, grep for orphan typedef class declarations and fail the build if any lack a matching full definition.
"Forward declaration in one package, full definition in another — type still incomplete"
DEBUGA user puts typedef class my_xact; in fwd_pkg and the full definition in xact_pkg, then is surprised that consumers of fwd_pkg cannot call methods on my_xact.
<code>// fwd_pkg.sv
package fwd_pkg;
typedef class my_xact;
endpackage
// xact_pkg.sv
package xact_pkg;
class my_xact;
function void show(); $display("xact"); endfunction
endclass
endpackage
// user.sv
import fwd_pkg::*;
function void use(my_xact x); x.show(); endfunction // ERROR</code>The forward declaration and the full definition must live in the same scope. The my_xact in fwd_pkg is a distinct, never-completed type from the my_xact in xact_pkg. fwd_pkg::my_xact remains forever incomplete.
Put both the forward and the full definition in the same package. Consumers then import that one package and get both the name resolution and the method visibility from a single import.
"Forward-declared parameterised class produces 'undeclared parameter' error"
DEBUGA user forward-declares typedef class fifo; and later defines class fifo #(type T = int);. Code using fifo#(byte) fails compilation.
<code>typedef class fifo; // BUG: forward doesn't capture parameters
class fifo #(type T = int);
T items[$];
endclass
fifo#(byte) f; // may fail depending on simulator strictness</code>The plain typedef class fifo; declares a non-parameterised forward, while the full definition is parameterised. Strict simulators reject this as a mismatch; lenient ones accept it but produce confusing downstream errors.
Forward-declare with matching parameters: typedef class fifo #(type T = int);. The forward and the definition must agree on the parameter list, just as they must agree on the class name and scope.
Interview Q&A — Twelve Questions You Will Be Asked
It introduces a forward declaration of a class — telling the compiler "a class with this name exists; its body will appear later." This lets you reference the class as a type (declare handles of it, pass it as an argument, return it) before its full definition is in scope. The primary use is breaking circular dependencies between classes that reference each other.
Best Practices — Ten Rules for Using typedef class Well
- Use
typedef classwhenever two classes legitimately need handles to each other. Request/response, callback/owner, monitor/scoreboard — break cycles with forwards rather than restructuring to avoid them. - Every
typedef class X;must be paired with a fullclass X; ... endclassin the same compilation unit. Orphan forwards compile silently and surface as cryptic "incomplete type" errors later. - Keep the forward and the full definition in the same scope. Forward in one package and definition in another are unrelated types; the forward stays incomplete forever.
- For parameterised classes, replicate the parameter list in the forward. A mismatch between forward and definition produces compile errors on every use site.
- Centralise forwards in a single header per package.
env_fwd.svhwith all cross-referencing class forwards; every implementation file includes it; full headers are pulled in only where construction or method calls occur. - Never use
class X; endclassas a forward declaration. That defines a class with zero members and conflicts with any later real definition.typedef class X;is the only correct forward form. - Use
externfunctions when a function body needs to call methods on a forward-declared class. Declare the signature early, define the body after the full class is visible. - Treat forward declarations as documentation of cycle structure. The set of forwards at the top of a header tells a reader which classes form a mutually-referencing group; that visibility helps reviewers understand the design.
- Don't forward-declare classes that aren't actually cyclic. If declaration order alone suffices, just declare them in order — adding a forward where it isn't needed is noise.
- Log both sides of every cross-class reference during bring-up. Cross-reference drift (req.matched_rsp points one way, rsp.orig_req points elsewhere) is the most common bug enabled by forward declarations — and the most invisible without explicit instrumentation.