Part 2 · OOP for Verification · Intermediate

The Transaction Base Class

The copy/compare/convert2string/pack discipline every transaction must implement, with a full bus_txn example.

Why every transaction implements the same four methods

A transaction object is created once but used everywhere : the generator randomizes it, the driver consumes it, the monitor reconstructs an observed copy, and the scoreboard compares expected against actual. Each of those uses needs a capability the language does not give classes for free: copy() (deep duplication — assignment only copies the handle), compare() (field-by-field equality — == on handles compares identity, not content), convert2string() (one-line debug rendering), and pack() / unpack() (serialization to a bit stream for pin-level driving or DPI transport).

The discipline is to implement all four on every transaction class, even when a particular bench does not yet call one of them. The first time a scoreboard mysteriously "passes" because it compared handles, or a monitor txn mutates because someone stored an alias instead of a copy, the value of the discipline becomes obvious — usually at 2 a.m. before tapeout.

diagram
WHY HANDLE SEMANTICS FORCE THE DISCIPLINE

  txn a = new();  a.addr = 'h10;
  txn b = a;                    // COPY? No — both handles  one object
  b.addr = 'h20;                // a.addr is now ALSO 'h20

      a ──┐
           ├──► [ object: addr='h20 ]      one object, two names
      b ──┘

  if (a == b)     compares HANDLES    true even if fields differ
  a.compare(b)    compares FIELDS     what the scoreboard needs
  b = a.copy()    second OBJECT       safe to mutate independently

      a ──► [ addr='h20 ]      b ──► [ addr='h20 ]   (detached twin)

The full bus_txn reference implementation

systemverilog
typedef enum bit { READ, WRITE } dir_e;

class bus_txn;
  rand dir_e        dir;
  rand bit [15:0]   addr;
  rand bit [31:0]   data;
  rand bit [3:0]    len;

  constraint c_len { len inside {[1:8]}; }

  // ---- copy: construct-then-fill, virtual so subclasses extend it ----
  virtual function bus_txn copy();
    bus_txn t = new();
    t.dir  = dir;
    t.addr = addr;
    t.data = data;
    t.len  = len;
    return t;
  endfunction

  // ---- compare: field equality, tolerant of null ----
  virtual function bit compare(bus_txn rhs);
    if (rhs == null) return 0;
    return (dir  == rhs.dir ) &&
           (addr == rhs.addr) &&
           (data == rhs.data) &&
           (len  == rhs.len );
  endfunction

  // ---- convert2string: ONE line, dense, greppable ----
  virtual function string convert2string();
    return $sformatf("%s addr=%04h data=%08h len=%0d",
                     dir.name(), addr, data, len);
  endfunction

  // ---- pack/unpack: fixed field order is the contract ----
  virtual function void pack(output bit bits[$]);
    bits = {};
    bits = {bits, dir};
    for (int i = 15; i >= 0; i--) bits.push_back(addr[i]);
    for (int i = 31; i >= 0; i--) bits.push_back(data[i]);
    for (int i = 3;  i >= 0; i--) bits.push_back(len[i]);
  endfunction

  virtual function void unpack(input bit bits[$]);
    int k = 0;
    dir = dir_e'(bits[k]); k++;
    for (int i = 15; i >= 0; i--) begin addr[i] = bits[k]; k++; end
    for (int i = 31; i >= 0; i--) begin data[i] = bits[k]; k++; end
    for (int i = 3;  i >= 0; i--) begin len[i]  = bits[k]; k++; end
  endfunction
endclass

Subclass extension — the part interviews probe

Every method is virtual so a derived transaction (say, an error-injecting subclass) can extend rather than break the contract: its copy() copies the new fields after delegating the base fields, and its compare() calls the base compare first. Without virtual, a scoreboard holding base-class handles would silently run the base methods on derived objects — comparing half the fields.

systemverilog
class err_txn extends bus_txn;
  rand bit force_err;

  virtual function bus_txn copy();
    err_txn t = new();
    void'(super_fields_into(t));    // or repeat base assignments
    t.force_err = force_err;
    return t;
  endfunction

  protected function bus_txn super_fields_into(bus_txn t);
    t.dir = dir; t.addr = addr; t.data = data; t.len = len;
    return t;
  endfunction
endclass

Usage in the dataflow — who calls what

  1. Generator: t = new(); t.randomize(); mb.put(t.copy()) — the copy detaches the queued object so re-randomizing t cannot corrupt it.

  2. Monitor: builds a FRESH txn from observed pins each transfer and sends a copy to subscribers — never a handle it will reuse.

  3. Scoreboard: exp.compare(act) decides pass/fail; on mismatch, prints exp.convert2string() and act.convert2string() side by side.

  4. Pin-level / DPI boundary: pack() flattens the txn into bits for a serial driver or a C-side checker; unpack() rebuilds it.

Interview angle

  • "What is wrong with b = a for objects?" — handle alias; mutation through either name hits the one object.

  • "Why must copy/compare be virtual?" — base-handle polymorphism: derived txns through base handles must run derived methods.

  • "This maps to which UVM methods?" — do_copy/do_compare/convert2string/do_pack under uvm_object, driven by field automation or manual overrides.

Key takeaways

  • Assignment copies handles, == compares handles — transactions need explicit copy() and compare().

  • Implement copy/compare/convert2string/pack on every txn class, virtual, from day one.

  • Subclasses delegate to the base implementation and extend it — never re-implement from scratch.

  • This is the plain-SV ancestor of uvm_object's do_copy/do_compare/do_pack contract.

Common pitfalls

  • Shallow-copying a txn that contains other class handles — the inner objects stay shared.

  • compare() that forgets a newly added field — scoreboard goes blind to bugs in exactly that field.

  • Non-virtual copy() in the base — derived objects copied via base handles silently lose their extra fields.

  • pack/unpack with mismatched field order — every value lands in the wrong field, sometimes plausibly.