Prototype Pattern in UVM: Cloning Transactions Without the Bugs

You've just built the perfect AXI transaction. The address is aligned, burst length is correct, cache attributes match the spec, protection bits are set, and it passes protocol checks cleanly. Now you need fifty variations of it — same structure, different addresses. Same burst type, different data. Same cache policy, different IDs. Do you build each one from scratch? Copy every field by hand? Or do you write axi_txn txn2 = txn1; and move on — not realizing you've just created a bug that won't surface until your scoreboard reports phantom mismatches three hours into regression?

The Prototype pattern gives you a safe, scalable way to clone objects. UVM already implements it through copy(), clone(), and do_copy(). This post shows you how they work, where the traps are, and how to use them to build reusable transaction templates that don't silently corrupt your testbench.

The Problem: Why Copying Objects is Harder Than You Think

You've built a golden AXI transaction — a 4-beat cacheable write burst to 0x1000 with the right ID, the right protection bits, and validated protocol fields. Now you need to send it to fifty different addresses. The obvious approach:

axi_txn golden = axi_txn::type_id::create("golden");
golden.op         = AXI_WRITE;
golden.addr       = 32'h0000_1000;
golden.burst_type = INCR;
golden.burst_len  = 3;
golden.burst_size = 3'b010;
golden.cache      = 4'b0011;
golden.prot       = 3'b000;
golden.id         = 8'h01;
golden.exp_resp   = OKAY;

// "Copy" it for a different address
axi_txn txn2 = golden;   // <-- This is NOT a copy
txn2.addr = 32'h0000_2000;

That last assignment — axi_txn txn2 = golden — is the trap. In SystemVerilog, this copies the handle, not the object. Both golden and txn2 now point to the same object in memory. When you set txn2.addr = 32'h0000_2000, you've also changed golden.addr. Your golden reference is gone.

graph LR
    subgraph "Handle Copy (THE BUG)"
        G1["golden"] --> OBJ1["axi_txn object\naddr = 0x2000"]
        T1["txn2"] --> OBJ1
    end
    subgraph "Object Copy (CORRECT)"
        G2["golden"] --> OBJ2["axi_txn object\naddr = 0x1000"]
        T2["txn2"] --> OBJ3["axi_txn object\naddr = 0x2000"]
    end
    style OBJ1 fill:#7f1d1d,stroke:#ef4444,color:#fca5a5
    style OBJ2 fill:#14532d,stroke:#22c55e,color:#bbf7d0
    style OBJ3 fill:#14532d,stroke:#22c55e,color:#bbf7d0

The handle copy diagram is exactly what happens when you write = with class handles in SystemVerilog. Two variables, one object. Every modification through either handle affects both.

The natural reaction is to copy field by field:

axi_txn txn2 = axi_txn::type_id::create("txn2");
txn2.op         = golden.op;
txn2.addr       = golden.addr;
txn2.burst_type = golden.burst_type;
txn2.burst_len  = golden.burst_len;
txn2.burst_size = golden.burst_size;
txn2.cache      = golden.cache;
txn2.prot       = golden.prot;
txn2.qos        = golden.qos;
txn2.lock       = golden.lock;
txn2.id         = golden.id;
txn2.exp_resp   = golden.exp_resp;
txn2.data       = golden.data;  // Don't forget data[]

txn2.addr = 32'h0000_2000;  // Now safe

This works, but it's fragile. Add a field to axi_txn next month — say, region or user — and every manual copy site silently becomes incomplete. The new field doesn't get copied, no compiler warning, and your clones drift out of sync with the original. In a testbench with dozens of sequences, good luck finding every place that copies an AXI transaction.

There's a pattern for this — and UVM already implements it.

Gang of Four: The Prototype Pattern

The Gang of Four defines the Prototype pattern as:

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Unlike Factory (which needs to know the class) or Builder (which needs to know the construction steps), Prototype needs just one thing: a working instance. You give it a fully configured object, and it makes copies. The copy carries all the state of the original — no construction knowledge required.

classDiagram
    class Prototype {
        <<interface>>
        +clone()* : Prototype
    }
    class ConcretePrototypeA {
        -field1
        -field2
        +clone() : Prototype
    }
    class ConcretePrototypeB {
        -fieldX
        -fieldY
        +clone() : Prototype
    }
    class Client {
        +operation(prototype : Prototype)
    }

    Prototype <|-- ConcretePrototypeA
    Prototype <|-- ConcretePrototypeB
    Client --> Prototype : clones

The key insight: the Client never needs to know which concrete class it's cloning. It calls clone() on the Prototype interface and gets a new, independent object of the correct runtime type. This is polymorphic copying.

Shallow vs Deep Copy

Prototype comes in two flavors, and the difference matters enormously:

  • Shallow copy — Duplicates the object's value-type fields (integers, enums, strings) but copies handles to nested objects by reference. The original and clone share nested objects.
  • Deep copy — Duplicates everything, including nested objects recursively. The original and clone are completely independent.

For a flat object with no class handles, shallow and deep are identical. The distinction only matters when your object contains handles to other objects — which, in real testbenches, it almost always does.

When Prototype Shines

  • Transaction templates — Build one golden reference, clone for each variation
  • Expensive initialization — Object takes many steps to configure; clone avoids repeating them
  • Runtime type preservation — Clone preserves the actual type, even through a base-class handle
  • Config snapshots — Capture a configuration object's state at a point in time
  • Sequence reuse — Store prototypes as class members, clone and modify in body()

UVM's Prototype: copy(), clone(), and do_copy()

UVM implements the Prototype pattern through three methods on uvm_object. They work together: copy() copies fields into an existing object, clone() creates a new object and copies into it, and do_copy() is your hook to define what "copy" means for your class.

copy() — Copy Into an Existing Object

The copy() method copies the state of one object into another already-created object:

axi_txn original = axi_txn::type_id::create("original");
original.op   = AXI_WRITE;
original.addr = 32'h0000_1000;
original.id   = 8'h01;

axi_txn duplicate = axi_txn::type_id::create("duplicate");
duplicate.copy(original);  // duplicate now has original's field values

// duplicate.addr == 32'h0000_1000
// duplicate.id   == 8'h01
// But duplicate is a SEPARATE object — safe to modify

copy() is a non-virtual method defined in uvm_object. It performs internal bookkeeping, then calls your do_copy() override. You never override copy() itself — you override do_copy().

clone() — Create New + Copy = Pure Prototype

The clone() method is the textbook Prototype operation: it creates a new object and copies the original's state into it in one step:

axi_txn original = axi_txn::type_id::create("original");
original.op   = AXI_WRITE;
original.addr = 32'h0000_1000;

// clone() returns uvm_object — must $cast to the actual type
uvm_object obj = original.clone();
axi_txn duplicate;
if (!$cast(duplicate, obj))
  `uvm_fatal("CAST", "clone() returned unexpected type")

// duplicate is a NEW object with original's field values

Notice the $cast requirement. clone() returns uvm_object, not your specific type. This is because clone() is defined at the uvm_object level and SystemVerilog doesn't support covariant return types. The $cast is unavoidable — but the clone itself preserves the runtime type. If you clone an axi_txn_with_coverage through a uvm_object handle, you get back an axi_txn_with_coverage.

Internally, clone() works roughly like this:

// Simplified uvm_object::clone()
function uvm_object clone();
  uvm_object obj;
  obj = this.create(get_name());  // Factory-aware creation
  obj.copy(this);                  // Copy all fields
  return obj;
endfunction

The create() call uses the UVM factory, which means factory overrides are respected. If axi_txn has been overridden with axi_txn_with_coverage, clone() produces an axi_txn_with_coverage — not the base type.

do_copy() — Your Override Hook

This is where you define what "copy" means for your class. do_copy() is called by copy() and, transitively, by clone(). You must call super.do_copy(rhs) first so that parent class fields get copied.

class axi_txn extends uvm_sequence_item;
  rand axi_op_e     op;
  rand bit [31:0]   addr;
  rand bit [31:0]   data[];
  rand axi_burst_e  burst_type;
  rand bit [7:0]    burst_len;
  rand bit [2:0]    burst_size;
  rand bit [3:0]    cache;
  rand bit [2:0]    prot;
  rand bit [3:0]    qos;
  rand bit          lock;
  rand bit [7:0]    id;
  rand axi_resp_e   exp_resp;

  `uvm_object_utils(axi_txn)

  function new(string name = "axi_txn");
    super.new(name);
  endfunction

  virtual function void do_copy(uvm_object rhs);
    axi_txn rhs_txn;
    super.do_copy(rhs);  // Always call super first

    if (!$cast(rhs_txn, rhs))
      `uvm_fatal("COPY", "Cast failed in do_copy")

    op         = rhs_txn.op;
    addr       = rhs_txn.addr;
    data       = rhs_txn.data;  // Dynamic array — value copy
    burst_type = rhs_txn.burst_type;
    burst_len  = rhs_txn.burst_len;
    burst_size = rhs_txn.burst_size;
    cache      = rhs_txn.cache;
    prot       = rhs_txn.prot;
    qos        = rhs_txn.qos;
    lock       = rhs_txn.lock;
    id         = rhs_txn.id;
    exp_resp   = rhs_txn.exp_resp;
  endfunction
endclass

Key details:

  • super.do_copy(rhs) must come first — it copies uvm_object / uvm_sequence_item base fields
  • The $cast converts the generic uvm_object rhs to your specific type so you can access its fields
  • Dynamic arrays of value types (like bit [31:0] data[]) are safe with = — SystemVerilog copies the array contents, not just the handle
  • Handles to other class objects are not safe with = — more on this in the deep vs shallow section

copy() vs clone() — When to Use Which

Aspectcopy()clone()
Creates new object?No — target must already existYes — creates via create()
Return typevoiduvm_object (requires $cast)
Typical useOverwrite an existing object's fieldsMake an independent duplicate
Factory-aware creation?N/A (no creation)Yes — calls create() internally
PerformanceSlightly faster (no allocation)Allocates a new object

Use clone() when you need a new independent object. Use copy() when you already have an object and want to overwrite its state — for example, snapshotting a transaction into a pre-allocated slot in a scoreboard.

Deep vs Shallow Copy: Where the Bugs Hide

Flat transactions with only value-type fields — integers, enums, packed arrays — are easy. The copy is complete and independent. But real testbench transactions are rarely flat. Consider an AXI transaction with a nested configuration object:

class axi_config extends uvm_object;
  rand bit [3:0] cache;
  rand bit [2:0] prot;
  rand bit [3:0] qos;
  rand bit       lock;

  `uvm_object_utils(axi_config)

  function new(string name = "axi_config");
    super.new(name);
  endfunction

  virtual function void do_copy(uvm_object rhs);
    axi_config rhs_cfg;
    super.do_copy(rhs);
    if (!$cast(rhs_cfg, rhs))
      `uvm_fatal("COPY", "Cast failed in axi_config::do_copy")
    cache = rhs_cfg.cache;
    prot  = rhs_cfg.prot;
    qos   = rhs_cfg.qos;
    lock  = rhs_cfg.lock;
  endfunction
endclass

class axi_txn extends uvm_sequence_item;
  rand axi_op_e     op;
  rand bit [31:0]   addr;
  rand bit [31:0]   data[];
  rand axi_burst_e  burst_type;
  rand bit [7:0]    burst_len;
  rand bit [2:0]    burst_size;
  rand bit [7:0]    id;
  rand axi_resp_e   exp_resp;

  axi_config        cfg;   // Nested object handle

  `uvm_object_utils(axi_txn)

  function new(string name = "axi_txn");
    super.new(name);
    cfg = axi_config::type_id::create("cfg");
  endfunction
endclass

Now watch what happens with a shallow copy:

// Shallow do_copy — copies the HANDLE, not the object
virtual function void do_copy(uvm_object rhs);
  axi_txn rhs_txn;
  super.do_copy(rhs);
  if (!$cast(rhs_txn, rhs)) `uvm_fatal("COPY", "Cast failed")

  op         = rhs_txn.op;
  addr       = rhs_txn.addr;
  data       = rhs_txn.data;
  burst_type = rhs_txn.burst_type;
  burst_len  = rhs_txn.burst_len;
  burst_size = rhs_txn.burst_size;
  id         = rhs_txn.id;
  exp_resp   = rhs_txn.exp_resp;
  cfg        = rhs_txn.cfg;   // BUG: shared handle!
endfunction
// The bug in action
axi_txn original = axi_txn::type_id::create("original");
original.cfg.cache = 4'b0011;  // Cacheable

uvm_object obj = original.clone();
axi_txn clone_txn;
$cast(clone_txn, obj);

// Modify the clone's config
clone_txn.cfg.cache = 4'b0000;  // Non-cacheable

// Check the original...
$display("original.cfg.cache = %b", original.cfg.cache);
// Output: original.cfg.cache = 0000    <-- CORRUPTED!

Both original and clone_txn share the same axi_config object. Modifying the config through either handle affects both transactions.

graph LR
    subgraph "Shallow Copy (SHARED CONFIG)"
        O1["original"] --> TX1["axi_txn\naddr = 0x1000"]
        C1["clone_txn"] --> TX2["axi_txn\naddr = 0x1000"]
        TX1 --> CFG1["axi_config\ncache = 0000"]
        TX2 --> CFG1
    end
    subgraph "Deep Copy (INDEPENDENT)"
        O2["original"] --> TX3["axi_txn\naddr = 0x1000"]
        C2["clone_txn"] --> TX4["axi_txn\naddr = 0x1000"]
        TX3 --> CFG2["axi_config\ncache = 0011"]
        TX4 --> CFG3["axi_config\ncache = 0000"]
    end
    style CFG1 fill:#7f1d1d,stroke:#ef4444,color:#fca5a5
    style CFG2 fill:#14532d,stroke:#22c55e,color:#bbf7d0
    style CFG3 fill:#14532d,stroke:#22c55e,color:#bbf7d0

The Fix: Clone Nested Handles in do_copy()

A proper deep copy clones every nested object handle rather than copying the handle directly:

// Deep do_copy — clones nested objects
virtual function void do_copy(uvm_object rhs);
  axi_txn rhs_txn;
  super.do_copy(rhs);
  if (!$cast(rhs_txn, rhs)) `uvm_fatal("COPY", "Cast failed")

  op         = rhs_txn.op;
  addr       = rhs_txn.addr;
  data       = rhs_txn.data;
  burst_type = rhs_txn.burst_type;
  burst_len  = rhs_txn.burst_len;
  burst_size = rhs_txn.burst_size;
  id         = rhs_txn.id;
  exp_resp   = rhs_txn.exp_resp;

  // Deep copy: clone the nested config object
  if (rhs_txn.cfg != null) begin
    uvm_object cfg_obj = rhs_txn.cfg.clone();
    if (!$cast(cfg, cfg_obj))
      `uvm_fatal("COPY", "Clone cast failed for cfg")
  end
  else begin
    cfg = null;
  end
endfunction

Now clone_txn.cfg and original.cfg are separate objects. Modifying one doesn't affect the other.

Dynamic Arrays and Queues

SystemVerilog's assignment behavior varies by type:

TypeAssignment via =Safe in do_copy()?
Integral types (bit, int, logic)Value copyYes
EnumsValue copyYes
StringsValue copyYes
Packed arraysValue copyYes
Dynamic arrays of value typesValue copy (copies contents)Yes
Queues of value typesValue copy (copies contents)Yes
Class handlesHandle copy (shared object)No — must clone
Dynamic arrays of class handlesCopies array, shares objectsNo — must clone each element

The general rule:

If a field is a class handle (or a container of class handles), you must explicitly clone it in do_copy(). Value types and their arrays are safe with plain assignment.

Practical Patterns: Transaction Templates & Test Reuse

Once you have a working do_copy(), the Prototype pattern unlocks several powerful verification patterns.

Golden Reference Transactions

Build a fully configured prototype once, then clone and modify the delta for each variation:

class axi_sweep_seq extends uvm_sequence #(axi_txn);
  `uvm_object_utils(axi_sweep_seq)

  function new(string name = "axi_sweep_seq");
    super.new(name);
  endfunction

  task body();
    axi_txn golden, txn;
    uvm_object obj;

    // Build the golden reference once
    golden = axi_txn::type_id::create("golden");
    golden.op         = AXI_WRITE;
    golden.burst_type = INCR;
    golden.burst_len  = 3;
    golden.burst_size = 3'b010;
    golden.cache      = 4'b0011;
    golden.prot       = 3'b000;
    golden.id         = 8'h01;
    golden.exp_resp   = OKAY;
    golden.data       = new[4];
    foreach (golden.data[i]) golden.data[i] = $urandom();

    // Clone and modify just the delta
    for (int i = 0; i < 50; i++) begin
      obj = golden.clone();
      if (!$cast(txn, obj))
        `uvm_fatal("CAST", "Clone cast failed")

      txn.addr = 32'h0000_1000 + (i * 32'h100);  // Sweep addresses
      txn.id   = i[7:0];
      txn.set_name($sformatf("txn_%0d", i));

      start_item(txn);
      finish_item(txn);
    end
  endtask
endclass

Fifty transactions, but the construction logic lives in one place. If you add a field to axi_txn, you update do_copy() and the golden reference — the clones automatically pick up the change.

Sequence Library Pattern

Store prototypes as sequence class members. Clone in body() for each send:

class axi_protocol_seq extends uvm_sequence #(axi_txn);
  `uvm_object_utils(axi_protocol_seq)

  // Prototypes — configured once in constructor or pre_body
  axi_txn write_proto;
  axi_txn read_proto;
  axi_txn excl_proto;

  function new(string name = "axi_protocol_seq");
    super.new(name);
  endfunction

  virtual task pre_body();
    // Build prototypes
    write_proto = axi_txn::type_id::create("write_proto");
    write_proto.op         = AXI_WRITE;
    write_proto.burst_type = INCR;
    write_proto.burst_len  = 3;
    write_proto.burst_size = 3'b010;
    write_proto.cache      = 4'b0011;
    write_proto.exp_resp   = OKAY;

    read_proto = axi_txn::type_id::create("read_proto");
    read_proto.op         = AXI_READ;
    read_proto.burst_type = INCR;
    read_proto.burst_len  = 3;
    read_proto.burst_size = 3'b010;
    read_proto.cache      = 4'b0011;
    read_proto.exp_resp   = OKAY;

    excl_proto = axi_txn::type_id::create("excl_proto");
    excl_proto.op         = AXI_WRITE;
    excl_proto.burst_type = INCR;
    excl_proto.burst_len  = 0;
    excl_proto.burst_size = 3'b010;
    excl_proto.lock       = 1;
    excl_proto.exp_resp   = EXOKAY;
  endtask

  task body();
    axi_txn txn;
    uvm_object obj;

    // Write-read pair to address 0x1000
    obj = write_proto.clone();
    $cast(txn, obj);
    txn.addr = 32'h0000_1000;
    txn.data = '{32'hAAAA, 32'hBBBB, 32'hCCCC, 32'hDDDD};
    `uvm_send(txn)

    obj = read_proto.clone();
    $cast(txn, obj);
    txn.addr = 32'h0000_1000;
    `uvm_send(txn)

    // Exclusive access to 0x2000
    obj = excl_proto.clone();
    $cast(txn, obj);
    txn.addr = 32'h0000_2000;
    txn.data = '{32'hDEAD_BEEF};
    `uvm_send(txn)
  endtask
endclass

The prototypes act as transaction templates. Each clone() produces an independent copy that you can modify without affecting the template. Test intent is clear: "same as the write prototype, but to address 0x1000."

Config Snapshots

Clone a configuration object to capture its state at a specific point in time — useful for scoreboards that need to compare actual behavior against the config that was active when a transaction was issued:

class axi_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(axi_scoreboard)

  axi_config cfg_snapshot;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // Capture config at the moment a transaction is sent
  function void snapshot_config(axi_config current_cfg);
    uvm_object obj = current_cfg.clone();
    if (!$cast(cfg_snapshot, obj))
      `uvm_fatal("CAST", "Config snapshot cast failed")
  endfunction

  // Later: compare against snapshot, not current config
  function void check_transaction(axi_txn txn);
    // Use cfg_snapshot — immune to subsequent config changes
    if (txn.cache != cfg_snapshot.cache)
      `uvm_error("SCRBD", "Cache mismatch vs config snapshot")
  endfunction
endclass

Prototype vs Build from Scratch

AspectPrototype (Clone + Modify)Build from Scratch
Setup effortBuild once, clone manyBuild each independently
Code duplicationLow — shared prototypeHigh — repeated field assignments
MaintenanceUpdate do_copy() + prototypeUpdate every construction site
Type preservationAutomatic — clone() preserves runtime typeMust use correct create() call
Readability"Like X, but with Y changed""Constructed with A, B, C, D, E, F..."
Best forMany similar objects with small deltasUnique objects with distinct configurations

Advanced: Prototype + Factory Working Together

The Factory and Prototype patterns are not competitors — they're collaborators. The Factory decides what type to create. The Prototype decides what state it starts with. Together, they enable polymorphic cloning with runtime type substitution.

Factory Creates, Prototype Copies

A common pattern: use the Factory to create the first instance (respecting overrides), then use Prototype to make copies:

class axi_traffic_gen extends uvm_component;
  `uvm_component_utils(axi_traffic_gen)

  axi_txn proto;  // Prototype — created once via factory

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // Factory creates the prototype — overrides apply here
    proto = axi_txn::type_id::create("proto");
    proto.op         = AXI_WRITE;
    proto.burst_type = INCR;
    proto.burst_len  = 3;
    proto.cache      = 4'b0011;
  endfunction

  task run_phase(uvm_phase phase);
    axi_txn txn;
    uvm_object obj;

    repeat (100) begin
      // Prototype copies — each clone inherits the factory override type
      obj = proto.clone();
      if (!$cast(txn, obj))
        `uvm_fatal("CAST", "Clone failed")
      txn.addr = $urandom_range(32'h0000_0000, 32'h0000_FFFF);
      // ... send txn
    end
  endtask
endclass

Polymorphic Cloning

Here is the key insight: clone() preserves the runtime type. If a Factory override replaces axi_txn with axi_txn_with_coverage, every clone() call produces axi_txn_with_coverage — even though the code only mentions axi_txn.

class axi_txn_with_coverage extends axi_txn;
  `uvm_object_utils(axi_txn_with_coverage)

  covergroup cg;
    cp_op:   coverpoint op;
    cp_burst: coverpoint burst_type;
    cp_cache: coverpoint cache;
  endgroup

  function new(string name = "axi_txn_with_coverage");
    super.new(name);
    cg = new();
  endfunction

  virtual function void do_copy(uvm_object rhs);
    super.do_copy(rhs);  // Copies all axi_txn fields
    // Coverage group is per-instance — no extra copy needed
  endfunction

  function void post_randomize();
    cg.sample();
  endfunction
endclass
sequenceDiagram
    participant Test
    participant Factory as uvm_factory
    participant Proto as proto : axi_txn
    participant Clone as clone : axi_txn_with_coverage

    Test->>Factory: set_type_override(axi_txn, axi_txn_with_coverage)
    Test->>Factory: axi_txn::type_id::create("proto")
    Factory-->>Test: proto (actually axi_txn_with_coverage)
    Note over Test,Proto: proto is configured with golden values
    Test->>Proto: proto.clone()
    Proto->>Proto: create() via factory
    Proto->>Clone: copy(this)
    Clone-->>Test: clone (axi_txn_with_coverage with golden values)
    Note over Test,Clone: clone has coverage + all golden field values

The sequence:

  1. A test overrides axi_txn with axi_txn_with_coverage via the Factory
  2. type_id::create() returns an axi_txn_with_coverage (Factory pattern)
  3. The prototype is configured with golden values
  4. clone() internally calls create(), which goes through the Factory — producing another axi_txn_with_coverage
  5. copy() transfers all field values from the prototype to the clone
  6. Every clone has both the golden field values and the coverage instrumentation

This is why clone() uses create() internally instead of new(). It ties the Prototype pattern back to the Factory, giving you type substitution and state copying in one operation. If you had used new axi_txn() instead, the factory override would be ignored and your clones would lack coverage.

Tying It All Together

Across this series, we've covered four creational patterns and how they work in UVM:

PatternWhat It ControlsUVM Mechanism
FactoryWhich type to createtype_id::create(), overrides
SingletonHow many instances existuvm_root, uvm_factory, uvm_coreservice_t
BuilderHow objects are constructedFluent interfaces, validation
PrototypeHow objects are copiedcopy(), clone(), do_copy()

These four patterns cover all the ways objects come into existence: the Factory picks the type, the Singleton ensures only one exists, the Builder constructs complex objects step by step, and the Prototype clones existing ones. With this foundation, the next series moves to Structural patterns — starting with the Adapter pattern, which bridges incompatible interfaces in your verification environment.

Quick Reference

Method Reference

MethodDefined InWhat It DoesOverride?
copy(rhs)uvm_objectCopies rhs fields into thisNo — override do_copy()
clone()uvm_objectCreates new object + copies fieldsNo
do_copy(rhs)uvm_objectUser hook for field-by-field copyYes — must call super.do_copy(rhs)
compare(rhs)uvm_objectCompares this with rhsOverride via do_compare()

Common Mistakes

MistakeFix
Using = to copy a transaction (txn2 = txn1)Use clone() or copy()= copies the handle, not the object
Forgetting $cast after clone()clone() returns uvm_object — always $cast to your type
Forgetting super.do_copy(rhs)Always call super.do_copy(rhs) first — parent fields won't be copied otherwise
Shallow-copying nested object handlesUse clone() on nested handles inside do_copy() for deep copy
Not implementing do_copy() at allWithout it, copy() and clone() only copy base uvm_object fields
Adding a field but not updating do_copy()Every new field must be added to do_copy() — clones will silently miss it otherwise
Overriding copy() instead of do_copy()copy() is non-virtual — override do_copy() for custom copy logic

Prototype vs Builder

AspectPrototypeBuilder
Core ideaCopy an existing objectConstruct step by step
Starting pointA fully configured instanceAn empty builder + method calls
Best forMany similar objects with small deltasComplex objects with validation rules
ValidationAssumes prototype is already validValidates during build()
Readability"Like X, but with Y changed""A write to 0x1000, cacheable, 4 beats"
UVM supportBuilt-in: copy(), clone(), do_copy()Custom builder class

Previous: Builder Pattern — Fluent interfaces, validation, and Directors for test scenarios

Next: Adapter Pattern — Bridging incompatible interfaces in your verification environment

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment