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

systemverilog
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.
diagram
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?'

systemverilog
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()");
  ...
endtask

Garbage 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.