Part 2 · OOP for Verification · Intermediate
Handles vs Objects
Reference semantics, null handles, aliasing on assignment, and garbage collection.
Reference semantics — the core mental model
A class variable in SystemVerilog is never the object itself. It is a handle — a safe pointer holding a reference to an object on the heap (or the special value null). Every operation you write on a class variable is really an operation through the reference: t.addr means 'follow the handle t, find the object, read its addr field'. This is identical to Java references, and unlike C++ stack objects — SystemVerilog objects are always heap-allocated and always accessed indirectly.
The consequence that catches everyone: assignment between handles copies the reference, not the object . After b = a; there is still exactly one object, now reachable through two names. Mutating through either handle is visible through both — they are aliases.
Handle assignment is aliasing
bus_txn a, b;
a = new(); // one object
a.addr = 32'h1000;
b = a; // NO new object — b now aliases the same one
b.addr = 32'h2000;
$display("%0h", a.addr); // prints 2000 — 'a' sees b's write!
// Identity vs equality:
if (a == b) $display("same object (handle compare)");
// a == b compares REFERENCES. For field-by-field equality
// you must write your own compare() method.ALIASING ON ASSIGNMENT
a = new(); a.addr = 'h1000;
a ──────┐
▼
┌──────────────┐
│ OBJ1 │
│ addr = 1000 │
└──────────────┘
b = a; b.addr = 'h2000;
a ──────┐ a ──────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ OBJ1 │ │ OBJ1 │
│ addr = 1000 │ │ addr = 2000 │ ◄── both see it
└──────────────┘ └──────────────┘
▲ ▲
b ──────┘ b ──────┘
ONE object, TWO names. No copy happened.Where aliasing bites in real testbenches
Generator re-randomizes a txn it already put in a mailbox — driver sees fields change mid-flight.
Monitor passes the same handle to scoreboard and coverage — one consumer mutating it corrupts the other.
Storing 'expected' in a scoreboard by handle while the source keeps mutating the object — compares against moving data.
null handles and the classic runtime crash
A declared handle starts as null — pointing at nothing. Dereferencing it (reading a property, calling a method) is a runtime fatal error , typically reported as a null object access with a stack trace. The compiler cannot catch it, because whether a handle is null is a runtime question. This is the single most common first-week crash in class-based testbenches, and a favorite interview question: 'your sim dies with NULL pointer dereference — what do you check?'
class env;
driver drv;
monitor mon;
function void build();
drv = new();
// forgot: mon = new();
endfunction
task run();
fork
drv.run();
mon.run(); // FATAL: null object access — mon was never constructed
join
endtask
endclass
// Defensive pattern at integration boundaries:
task run();
if (mon == null) $fatal(1, "env.mon not constructed — check build()");
...
endtaskGarbage collection
SystemVerilog is garbage-collected: when the last handle to an object goes away — reassigned, set to null, or out of scope — the object becomes unreachable and the simulator reclaims it automatically. There is no delete on class objects and no dangling-pointer hazard: a handle is either null or points at a live object. The flip side is retention : any surviving reference (a queue entry, a mailbox, a static variable) keeps the object alive, which is how testbench memory leaks happen — covered in the Object Lifetime sub-topic.
Key takeaways
Class variables are references; assignment aliases, it never copies the object.
== on handles compares identity (same object), not field equality — write compare() for that.
Dereferencing null is a runtime fatal; the usual cause is a missing new() in a build/construct step.
Garbage collection frees objects when the last reference disappears — no delete, no dangling handles.
Common pitfalls
b = a then mutating b expecting a to be untouched — they alias one object.
Using == to compare transaction contents — passes only when both handles point at the same object.
Calling a method on a component you forgot to construct — null dereference deep in run, far from the missing new().
Assuming an object is gone after one handle is nulled — other references (queues, mailboxes) keep it alive.