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
- Gang of Four: The Prototype Pattern
- UVM's Prototype: copy(), clone(), and do_copy()
- Deep vs Shallow Copy: Where the Bugs Hide
- Practical Patterns: Transaction Templates & Test Reuse
- Advanced: Prototype + Factory Working Together
- Quick Reference
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 copiesuvm_object/uvm_sequence_itembase fields- The
$castconverts the genericuvm_object rhsto 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
| Aspect | copy() | clone() |
|---|---|---|
| Creates new object? | No — target must already exist | Yes — creates via create() |
| Return type | void | uvm_object (requires $cast) |
| Typical use | Overwrite an existing object's fields | Make an independent duplicate |
| Factory-aware creation? | N/A (no creation) | Yes — calls create() internally |
| Performance | Slightly 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:
| Type | Assignment via = | Safe in do_copy()? |
|---|---|---|
Integral types (bit, int, logic) | Value copy | Yes |
| Enums | Value copy | Yes |
| Strings | Value copy | Yes |
| Packed arrays | Value copy | Yes |
| Dynamic arrays of value types | Value copy (copies contents) | Yes |
| Queues of value types | Value copy (copies contents) | Yes |
| Class handles | Handle copy (shared object) | No — must clone |
| Dynamic arrays of class handles | Copies array, shares objects | No — 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
| Aspect | Prototype (Clone + Modify) | Build from Scratch |
|---|---|---|
| Setup effort | Build once, clone many | Build each independently |
| Code duplication | Low — shared prototype | High — repeated field assignments |
| Maintenance | Update do_copy() + prototype | Update every construction site |
| Type preservation | Automatic — clone() preserves runtime type | Must use correct create() call |
| Readability | "Like X, but with Y changed" | "Constructed with A, B, C, D, E, F..." |
| Best for | Many similar objects with small deltas | Unique 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:
- A test overrides
axi_txnwithaxi_txn_with_coveragevia the Factory type_id::create()returns anaxi_txn_with_coverage(Factory pattern)- The prototype is configured with golden values
clone()internally callscreate(), which goes through the Factory — producing anotheraxi_txn_with_coveragecopy()transfers all field values from the prototype to the clone- 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:
| Pattern | What It Controls | UVM Mechanism |
|---|---|---|
| Factory | Which type to create | type_id::create(), overrides |
| Singleton | How many instances exist | uvm_root, uvm_factory, uvm_coreservice_t |
| Builder | How objects are constructed | Fluent interfaces, validation |
| Prototype | How objects are copied | copy(), 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
| Method | Defined In | What It Does | Override? |
|---|---|---|---|
copy(rhs) | uvm_object | Copies rhs fields into this | No — override do_copy() |
clone() | uvm_object | Creates new object + copies fields | No |
do_copy(rhs) | uvm_object | User hook for field-by-field copy | Yes — must call super.do_copy(rhs) |
compare(rhs) | uvm_object | Compares this with rhs | Override via do_compare() |
Common Mistakes
| Mistake | Fix |
|---|---|
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 handles | Use clone() on nested handles inside do_copy() for deep copy |
Not implementing do_copy() at all | Without 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
| Aspect | Prototype | Builder |
|---|---|---|
| Core idea | Copy an existing object | Construct step by step |
| Starting point | A fully configured instance | An empty builder + method calls |
| Best for | Many similar objects with small deltas | Complex objects with validation rules |
| Validation | Assumes prototype is already valid | Validates during build() |
| Readability | "Like X, but with Y changed" | "A write to 0x1000, cacheable, 4 beats" |
| UVM support | Built-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
Comments (0)
Leave a Comment