UVM
Field Macros — `uvm_field_*
What field macros automate, complete catalogue, flag options, critical limitations, performance cost.
UVM Fundamentals · Module 16
What Field Macros Do — The Six Auto-Generated Methods
A uvm_sequence_item needs to support printing, copying, comparing, packing, unpacking, and recording. Without field macros you write all six yourself. With field macros you declare each field once and the framework generates all six.
Figure 1 — Field macros auto-generate six object automation methods. One `uvm_field_* line per field replaces 30–50 lines of manual do_copy/do_compare/do_print/do_pack/do_unpack/do_record code.
The Complete Field Macro Catalogue
| Macro | Used For | Example |
|---|---|---|
| ``uvm_field_int(NAME, FLAG)` | All integral types: bit, logic, int, byte, integer, longint, packed structs | ``uvm_field_int(addr, UVM_ALL_ON)` |
| ``uvm_field_string(NAME, FLAG)` | string fields | ``uvm_field_string(tag, UVM_ALL_ON)` |
| ``uvm_field_object(NAME, FLAG)` | uvm_object sub-objects (handles) | ``uvm_field_object(sub_pkt, UVM_ALL_ON)` |
| ``uvm_field_real(NAME, FLAG)` | real fields (note: no packing) | ``uvm_field_real(voltage, UVM_ALL_ON)` |
| ``uvm_field_enum(TYPE, NAME, FLAG)` | Enum fields — requires the enum type as first arg | ``uvm_field_enum(opcode_t, op, UVM_ALL_ON)` |
| ``uvm_field_array_int(NAME, FLAG)` | Fixed or dynamic arrays of integers | ``uvm_field_array_int(byte_arr, UVM_ALL_ON)` |
| ``uvm_field_array_object(NAME, FLAG)` | Arrays of uvm_object handles | ``uvm_field_array_object(pkts, UVM_ALL_ON)` |
| ``uvm_field_array_string(NAME, FLAG)` | Arrays of strings | ``uvm_field_array_string(tags, UVM_ALL_ON)` |
| ``uvm_field_queue_int(NAME, FLAG)` | Queues of integers | ``uvm_field_queue_int(data_q, UVM_ALL_ON)` |
| ``uvm_field_queue_object(NAME, FLAG)` | Queues of uvm_object handles | ``uvm_field_queue_object(pkt_q, UVM_ALL_ON)` |
| ``uvm_field_aa_int_string(NAME, FLAG)` | Associative array: int[string] | ``uvm_field_aa_int_string(reg_map, UVM_ALL_ON)` |
| ``uvm_field_aa_object_string(NAME, FLAG)` | Associative array: uvm_object[string] | ``uvm_field_aa_object_string(db, UVM_ALL_ON)` |
| ``uvm_field_event(NAME, FLAG)` | SystemVerilog events (print/record only) | ``uvm_field_event(done_ev, UVM_ALL_ON)` |
The begin/end Pattern — Every Field Macro Needs a Wrapper
Field macros must be enclosed between uvm_object_utils_begin` and uvm_object_utils_end (for objects) or ``uvm_component_utils_begin and uvm_component_utils_end` (for components). The simple uvm_object_utils(class)` macro is equivalent to begin/end with no fields in between.
typedef enum bit { WRITE=0, READ=1 } dir_e;
class apb_txn extends uvm_sequence_item;
// ── Field declarations ────────────────────────────────────────────
rand bit [31:0] addr;
rand bit [31:0] data;
rand dir_e direction;
rand bit [3:0] strobe; // byte enables
bit [1:0] resp; // not randomised
string tag; // debug label
bit [7:0] data_q[$]; // payload queue
// ── Factory registration with field automation ────────────────────
`uvm_object_utils_begin(apb_txn)
`uvm_field_int (addr, UVM_ALL_ON)
`uvm_field_int (data, UVM_ALL_ON)
`uvm_field_enum (dir_e, direction, UVM_ALL_ON)
`uvm_field_int (strobe, UVM_ALL_ON)
`uvm_field_int (resp, UVM_NOCOMPARE | UVM_PRINT)
`uvm_field_string(tag, UVM_NOCOMPARE | UVM_PRINT)
`uvm_field_queue_int(data_q, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "apb_txn");
super.new(name);
endfunction
constraint c_valid_addr { addr[1:0] == 2'b00; } // word-aligned
endclass
// ── What you get for free — all without writing any of these manually:
apb_txn a = apb_txn::type_id::create("a");
apb_txn b = apb_txn::type_id::create("b");
void'(a.randomize());
a.print(); // formatted table via do_print
b.copy(a); // deep copy via do_copy
`uvm_info("T", $sformatf("%0d", a.compare(b)), UVM_LOW) // 1 = match
bit [7:0] packed[$];
void'(a.pack_bytes(packed)); // serialize to bytes
b.unpack_bytes(packed); // deserialize backFlags and Options — Controlling Each Field's Behaviour
The second argument to every ``uvm_field_*macro is a flag that controls which methods include this field. Flags are bit masks combined with|`.
| Flag | Value | Effect |
|---|---|---|
UVM_ALL_ON | 0x3FF | Include field in all six operations. The standard choice for most fields. |
UVM_COPY | 1 << 0 = 0x001 | Include in do_copy() |
UVM_NOCOPY | 1 << 1 = 0x002 | Exclude from do_copy() |
UVM_COMPARE | 1 << 2 = 0x004 | Include in do_compare() |
UVM_NOCOMPARE | 1 << 3 = 0x008 | Exclude from do_compare() — use for debug fields like tag, timestamp, resp |
UVM_PRINT | 1 << 4 = 0x010 | Include in do_print() |
UVM_NOPRINT | 1 << 5 = 0x020 | Exclude from print — use for large arrays or sensitive data |
UVM_PACK | 0x100 | Include in do_pack() / do_unpack() |
UVM_NOPACK | 1 << 9 = 0x200 | Exclude from pack — use for metadata that doesn't travel with the packet |
UVM_DEFAULT | 0x1FF | The combined "everything on" flag — copy, compare, print, record and pack |
UVM_READONLY | print only | Print only — no copy, compare, or pack. Good for internal counters and timestamps. |
UVM_HEX | display mode | Print value as hexadecimal. Combine: UVM_ALL_ON | UVM_HEX |
UVM_DEC | display mode | Print value as decimal. |
UVM_BIN | display mode | Print value as binary. |
`uvm_object_utils_begin(my_txn)
// ── Payload fields: copy, compare, pack, print ────────────────────
`uvm_field_int(addr, UVM_ALL_ON | UVM_HEX) // print as 0x...
`uvm_field_int(data, UVM_ALL_ON | UVM_HEX)
`uvm_field_int(strobe, UVM_ALL_ON | UVM_BIN) // print as 0b...
// ── Control field: all ops but show as decimal ────────────────────
`uvm_field_int(burst_len, UVM_ALL_ON | UVM_DEC)
// ── Response field: do not compare, do not pack — just print ─────
`uvm_field_int(resp, UVM_NOCOMPARE | UVM_NOPACK | UVM_PRINT | UVM_COPY)
// ── Debug tag: print only — never affects compare or copy ─────────
`uvm_field_string(debug_tag, UVM_READONLY)
// ── Timestamp: print only, not compared, not packed ───────────────
`uvm_field_int(timestamp, UVM_READONLY)
// ── Sub-object: include in copy+compare, exclude from pack ────────
`uvm_field_object(header_pkt, UVM_ALL_ON | UVM_NOPACK)
// ── Queue: all operations ─────────────────────────────────────────
`uvm_field_queue_int(payload_bytes, UVM_ALL_ON)
// ── Enum: remember the type comes first ───────────────────────────
`uvm_field_enum(opcode_t, opcode, UVM_ALL_ON)
`uvm_object_utils_endWhat Gets Generated — The Simplified Expansion
Understanding what the macros expand to helps you predict their behaviour and know when they will not do what you expect.
// `uvm_field_int(addr, UVM_ALL_ON) expands to contributions inside each
// of the automation framework methods. Simplified equivalent:
// Contribution to do_copy:
function void do_copy(uvm_object rhs);
apb_txn rhs_;
$cast(rhs_, rhs);
super.do_copy(rhs);
this.addr = rhs_.addr; // ← addr field contribution
// ... data, direction, strobe ...
endfunction
// Contribution to do_compare:
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
apb_txn rhs_;
$cast(rhs_, rhs);
return super.do_compare(rhs, comparer) &&
comparer.compare_field("addr", this.addr, rhs_.addr, $bits(addr));
endfunction
// Contribution to do_print:
function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field("addr", this.addr, $bits(addr), UVM_HEX);
endfunction
// Contribution to do_pack/do_unpack:
function void do_pack(uvm_packer packer);
super.do_pack(packer);
packer.pack_field(this.addr, $bits(addr));
endfunction
function void do_unpack(uvm_packer packer);
super.do_unpack(packer);
this.addr = packer.unpack_field_int($bits(addr));
endfunction
// The actual macro expansion uses a dispatch table with virtual methods —
// it goes through uvm_field_automation() which adds runtime dispatch overhead.
// This is why field macros are slower than direct do_* implementations.Limitations — When to Stop Using Field Macros
Side-by-Side: With Macros vs Manual do_* Methods
With field macros — convenient, and fixed in what it can express:
class apb_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_txn)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_object_utils_end
rand bit [31:0] addr, data;
function new(string n="apb_txn"); super.new(n); endfunction
endclass
// What you give up:
// 1. Generic, introspective comparison on every compare/copy call
// 2. No sub-range comparison — cannot compare addr[15:0] only
// 3. No conditional logic — cannot skip a field based on ignore_mask
// 4. Display formatting is whatever the flag selects
// 5. Packed structs and unions need workaroundsManual do_* — explicit, and able to express the thing the macros cannot:
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)
rand bit [31:0] addr, data;
bit [31:0] addr_mask = 32'hFFFF_FFFF;
function new(string n="apb_txn"); super.new(n); endfunction
function void do_copy(uvm_object rhs);
apb_txn r;
if (!$cast(r, rhs)) `uvm_fatal("COPY", "type mismatch in do_copy")
super.do_copy(rhs);
addr = r.addr;
data = r.data;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
apb_txn r;
bit ok = 1;
if (!$cast(r, rhs)) `uvm_fatal("CMP", "type mismatch in do_compare")
ok &= super.do_compare(rhs, comparer);
// The point of going manual: compare only the masked address bits.
ok &= comparer.compare_field("addr", addr & addr_mask,
r.addr & r.addr_mask, 32);
ok &= comparer.compare_field("data", data, r.data, 32);
return ok;
endfunction
endclassNote &= rather than && in do_compare. && short-circuits, so the first differing field prevents every later compare_field call from running — and running is what records a miscompare with the comparer. do_copy and do_compare covers that trap in full.
Ready-to-Run Simulator Example
Complete demonstration: an APB transaction with field macros, showing print(), compare(), clone(), pack_bytes(), and the effect of selective flags. Copy to field_macros_demo.sv and run.
Ready to Run — Questa / VCS / Xcelium
// field_macros_demo.sv — complete ready-to-run UVM field macro demonstration
//
// Compile and run:
// Questa : vlog -sv field_macros_demo.sv &&
// vsim -c field_macros_top -do "run -all; quit"
// VCS : vcs -sverilog -ntb_opts uvm field_macros_demo.sv && ./simv
// Xcelium: xrun -sv -uvm field_macros_demo.sv -input "run; exit"
`include "uvm_macros.svh"
import uvm_pkg::*;
// ═══════════════════════════════════════════════════════════════════════
// ENUM TYPE (must be defined outside the class)
// ═══════════════════════════════════════════════════════════════════════
typedef enum bit { APB_WRITE=0, APB_READ=1 } apb_dir_e;
// ═══════════════════════════════════════════════════════════════════════
// TRANSACTION WITH FIELD MACROS
// ═══════════════════════════════════════════════════════════════════════
class apb_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand apb_dir_e direction;
rand bit [3:0] strobe;
bit [1:0] pslverr; // response — excluded from compare
string debug_tag; // label — excluded from compare
bit [7:0] payload[$]; // byte queue
time issue_time; // readonly — print only
`uvm_object_utils_begin(apb_txn)
`uvm_field_int (addr, UVM_ALL_ON | UVM_HEX)
`uvm_field_int (data, UVM_ALL_ON | UVM_HEX)
`uvm_field_enum (apb_dir_e, direction, UVM_ALL_ON)
`uvm_field_int (strobe, UVM_ALL_ON | UVM_BIN)
`uvm_field_int (pslverr, UVM_NOCOMPARE | UVM_PRINT | UVM_COPY)
`uvm_field_string (debug_tag, UVM_NOCOMPARE | UVM_PRINT)
`uvm_field_queue_int(payload, UVM_ALL_ON)
`uvm_field_int (issue_time,UVM_READONLY)
`uvm_object_utils_end
function new(string name = "apb_txn");
super.new(name);
endfunction
constraint c_align { addr[1:0] == 2'b00; }
constraint c_strobe { strobe != 4'h0; }
endclass
// ═══════════════════════════════════════════════════════════════════════
// TEST — exercises all field macro capabilities
// ═══════════════════════════════════════════════════════════════════════
class field_macro_test extends uvm_test;
`uvm_component_utils(field_macro_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
apb_txn a, b, c;
bit [7:0] packed_bytes[$];
phase.raise_objection(this);
// ── Create and randomize ──────────────────────────────────────
a = apb_txn::type_id::create("txn_a");
void'(a.randomize() with { addr == 32'hA000_0000; direction == APB_WRITE; });
a.data = 32'h1234_5678;
a.pslverr = 2'b00;
a.debug_tag = "test_write_1";
a.issue_time = $time;
a.payload = { 8'h12, 8'h34, 8'h56, 8'h78 };
// ── print(): shows formatted table ────────────────────────────
`uvm_info("TEST", "=== print() output ===", UVM_NONE)
a.print();
// ── clone(): deep copy ────────────────────────────────────────
$cast(b, a.clone());
b.set_name("txn_b_clone");
`uvm_info("TEST", "=== compare() after clone — must be 1 ===", UVM_NONE)
`uvm_info("TEST", $sformatf("a.compare(b) = %0d", a.compare(b)), UVM_NONE)
// ── Modify and compare — should show mismatch ─────────────────
b.data = 32'hFFFF_0000;
`uvm_info("TEST", "=== compare() after data change — must be 0 ===", UVM_NONE)
`uvm_info("TEST", $sformatf("a.compare(b) = %0d", a.compare(b)), UVM_NONE)
// ── pslverr is NOCOMPARE — change it, compare still passes ────
$cast(c, a.clone());
c.pslverr = 2'b11; // error response
`uvm_info("TEST", "=== compare() with different pslverr — must be 1 (NOCOMPARE) ===", UVM_NONE)
`uvm_info("TEST", $sformatf("a.compare(c) = %0d", a.compare(c)), UVM_NONE)
// ── pack_bytes(): serialize to byte array ─────────────────────
void'(a.pack_bytes(packed_bytes));
`uvm_info("TEST",
$sformatf("pack_bytes() produced %0d bytes", packed_bytes.size()),
UVM_NONE)
// ── unpack_bytes(): deserialize back ──────────────────────────
b.data = 0; b.addr = 0;
void'(b.unpack_bytes(packed_bytes));
`uvm_info("TEST",
$sformatf("After unpack: addr=0x%0h data=0x%0h", b.addr, b.data),
UVM_NONE)
// ── sprint(): get the print as a string ───────────────────────
`uvm_info("TEST", "=== sprint() into string ===", UVM_NONE)
`uvm_info("TEST", a.sprint(), UVM_NONE)
`uvm_info("TEST", "=== All field macro tests passed ===", UVM_NONE)
phase.drop_objection(this);
endtask
endclass
// ═══════════════════════════════════════════════════════════════════════
// TOP MODULE
// ═══════════════════════════════════════════════════════════════════════
module field_macros_top;
initial run_test("field_macro_test");
endmodule
// ═══════════════════════════════════════════════════════════════════════
// EXPECTED OUTPUT (abridged):
//
// TEST: === print() output ===
// -----------------------------------------------
// Name Type Size Value
// -----------------------------------------------
// txn_a apb_txn - @1
// addr integral 32 'ha0000000
// data integral 32 'h12345678
// direction apb_dir_e 1 APB_WRITE
// strobe integral 4 'b1111
// pslverr integral 2 'h0
// debug_tag string 12 test_write_1
// payload da(integral) 4 ...
// issue_time integral 64 'h0 (readonly)
// -----------------------------------------------
//
// TEST: compare() after clone — must be 1
// TEST: a.compare(b) = 1
//
// TEST: compare() after data change — must be 0
// TEST: a.compare(b) = 0
//
// TEST: compare() with different pslverr — must be 1 (NOCOMPARE)
// TEST: a.compare(c) = 1
//
// TEST: pack_bytes() produced N bytes
// TEST: After unpack: addr=0xa0000000 data=0x12345678
// TEST: === All field macro tests passed ===
// ═══════════════════════════════════════════════════════════════════════## ── Questa ────────────────────────────────────────────────────────────
vlog -sv -timescale 1ns/1ps field_macros_demo.sv
vsim -c field_macros_top +UVM_VERBOSITY=UVM_NONE \
-do "run -all; quit -f"
## ── VCS ───────────────────────────────────────────────────────────────
vcs -sverilog -ntb_opts uvm -timescale=1ns/1ps field_macros_demo.sv -o simv
./simv +UVM_TESTNAME=field_macro_test +UVM_VERBOSITY=UVM_NONE
## ── Xcelium ───────────────────────────────────────────────────────────
xrun -sv -uvm -timescale 1ns/1ps field_macros_demo.sv \
-input "run; exit" \
+UVM_TESTNAME=field_macro_test +UVM_VERBOSITY=UVM_NONE
## ── To see UVM report summary:
## Remove +UVM_VERBOSITY=UVM_NONE from any command aboveQuick Reference
| Task | Syntax |
|---|---|
| Wrap field macros (object) | ``uvm_object_utils_begin(class_name) ... uvm_object_utils_end |
| Wrap field macros (component) | ``uvm_component_utils_begin(class_name) ... uvm_component_utils_end |
| Integer field (all ops) | ``uvm_field_int(field_name, UVM_ALL_ON)` |
| Integer field (hex display) | ``uvm_field_int(field_name, UVM_ALL_ON | UVM_HEX)` |
| Enum field | ``uvm_field_enum(enum_type, field_name, UVM_ALL_ON)` |
| String field | ``uvm_field_string(field_name, UVM_ALL_ON)` |
| Object field (sub-object) | ``uvm_field_object(field_name, UVM_ALL_ON)` |
| Queue of integers | ``uvm_field_queue_int(field_name, UVM_ALL_ON)` |
| Exclude from compare (timestamps, responses) | UVM_NOCOMPARE | UVM_PRINT | UVM_COPY |
| Print only (read-only metadata) | UVM_READONLY |
| Print and call all auto methods | txn.print() / s = txn.sprint() |
| Clone (deep copy to new object) | $cast(clone_h, txn.clone()) |
| Pack to bytes | void'(txn.pack_bytes(byte_queue)) |
| Unpack from bytes | void'(txn.unpack_bytes(byte_queue)) |
§9 — Code Examples
Example 1 — Beginner: Minimal APB Transaction with Field Macros
The most common starting point. Three fields, three macros, and you get print(), copy(), compare(), and pack/unpack — plus convert2string() and record() — all for free. This is what the generated output actually looks like.
class apb_seq_item extends uvm_sequence_item;
`uvm_object_utils_begin(apb_seq_item)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(write, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string name="apb_seq_item"); super.new(name); endfunction
endclass
// ── What each generated method produces ──────────────────────────────
//
// txn.print():
// --------------------------------------------------------
// Name Type Size Value
// --------------------------------------------------------
// apb_seq_item object -
// addr integral 32 'h4000
// data integral 32 'hA5A5
// write integral 1 'h1
//
// txn.convert2string():
// "addr='h4000 data='ha5a5 write='h1"
//
// txn1.compare(txn2):
// Returns 1 if all three fields match, 0 otherwise.
// Prints mismatch detail if +UVM_REPORT_VERBOSITY=UVM_HIGH
//
// txn_copy = txn.clone(): deep copy of all three fields (new object)
//
// txn.pack_ints(arr):
// Packs addr[31:0] + data[31:0] + write[0] = 65 bits into integer array
// ── Scoreboard compare using auto-generated compare() ────────────────
function void write(apb_seq_item actual);
apb_seq_item expected;
// ... get expected ...
if (!actual.compare(expected)) begin
`uvm_error("SCB", $sformatf("Mismatch:\n actual: %s\n expected: %s",
actual.convert2string(), expected.convert2string()))
end
endfunctionExample 2 — Intermediate: Flags Controlling Comparison and Print
The UVM_DEFAULT flag is actually a combination. Breaking it apart gives you precise control over which operations include which fields.
// UVM_DEFAULT = UVM_ALL_ON = 0x01FF
// Deconstructed: COPY | COMPARE | PRINT | RECORD | PACK | UNPACK | NORADIX
//
// Selectively disable specific operations per field:
class packet extends uvm_sequence_item;
`uvm_object_utils_begin(packet)
// addr: full default — compare, print, copy, pack all enabled
`uvm_field_int(addr, UVM_DEFAULT)
// data: compared and printed, but EXCLUDED from pack/unpack
`uvm_field_int(data, UVM_DEFAULT | UVM_NOPACK)
// timestamp: printed (for debugging) but NOT compared
// Timestamps differ between expected and actual — compare must skip it
`uvm_field_int(timestamp, UVM_DEFAULT | UVM_NOCOMPARE)
// debug_tag: internal debug only — never printed in clean regression logs
`uvm_field_int(debug_tag, UVM_COPY | UVM_COMPARE)
// UVM_COPY | UVM_COMPARE: copy and compare but don't print
// status: excluded from comparison entirely (DUT-side, cannot predict)
`uvm_field_int(status, UVM_DEFAULT | UVM_NOCOMPARE)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
logic [63:0] timestamp;
logic [7:0] debug_tag;
logic [3:0] status;
function new(string n="packet"); super.new(n); endfunction
endclass
// ── Common flag values ────────────────────────────────────────────────
//
// UVM_DEFAULT = 0x01FF All operations enabled (copy+compare+print+pack+...)
// UVM_NOCOMPARE = 0x0008 Exclude from compare() (bit 3)
// UVM_NOPRINT = 0x0010 Exclude from print()
// UVM_NOPRINTX = 0x0010 (same as NOPRINT in most implementations)
// UVM_NOPACK = 0x0100 Exclude from pack/unpack
// UVM_NOCOPY = 0x0001 Exclude from copy() — rarely used
// UVM_COPY = 0x0001 Explicitly enable copy
// UVM_COMPARE = 0x0004 Explicitly enable compare
// UVM_PRINT = 0x0008 Explicitly enable printExample 3 — Verification: Dynamic Array and Queue Fields
Field macros handle dynamic arrays and queues natively — but the behaviour differs from what engineers expect when the array size changes between operations.
class burst_txn extends uvm_sequence_item;
`uvm_object_utils_begin(burst_txn)
`uvm_field_int(start_addr, UVM_DEFAULT)
// Dynamic array of 32-bit data words
`uvm_field_array_int(data_words, UVM_DEFAULT)
// Queue of byte-sized error flags
`uvm_field_queue_int(error_flags, UVM_DEFAULT)
// Enum field
`uvm_field_enum(burst_type_e, burst_type, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] start_addr;
rand logic [31:0] data_words[]; // dynamic array
rand logic [7:0] error_flags[$]; // queue
rand burst_type_e burst_type;
function new(string n="burst_txn"); super.new(n); endfunction
endclass
// ── What compare() does with arrays ──────────────────────────────────
// 1. Compares array SIZE first — different sizes → immediate mismatch
// 2. Compares elements index by index
// 3. Prints element-level mismatch detail at UVM_HIGH verbosity
//
// ── What copy() does with dynamic arrays ─────────────────────────────
// Creates a new dynamic array of the same size and copies each element.
// This IS a deep copy for int/logic arrays — each element is a value type.
// For object arrays (uvm_field_array_object), copy() does SHALLOW copy
// — each element is still a shared handle. Use do_copy() for true deep copy.
//
// ── Tricky: compare() on different-length arrays ─────────────────────
burst_txn a = burst_txn::type_id::create("a");
burst_txn b = burst_txn::type_id::create("b");
a.data_words = new[4]('{1,2,3,4});
b.data_words = new[3]('{1,2,3});
// a.compare(b) → 0 (FAIL) — size mismatch detected first
// Error: "data_words: size mismatch: lhs=4 rhs=3"Example 4 — Tricky: Object Handle Field — The Shallow Copy Trap
The most dangerous field macro behaviour. When a transaction contains a handle to another object, ``uvm_field_object` does a shallow copy by default. Both the original and the copy point to the same sub-object. Modifying one corrupts the other.
class header extends uvm_object;
`uvm_object_utils_begin(header)
`uvm_field_int(hdr_type, UVM_DEFAULT)
`uvm_object_utils_end
rand int hdr_type;
function new(string n="header"); super.new(n); endfunction
endclass
class complex_txn extends uvm_sequence_item;
`uvm_object_utils_begin(complex_txn)
// ❌ Shallow copy — both original and copy share the SAME header object!
`uvm_field_object(hdr, UVM_DEFAULT)
`uvm_field_int(payload, UVM_DEFAULT)
`uvm_object_utils_end
header hdr;
rand int payload;
function new(string n="complex_txn"); super.new(n); endfunction
endclass
// ── Demonstration of the shallow copy problem ────────────────────────
complex_txn orig = complex_txn::type_id::create("orig");
complex_txn copy;
orig.hdr = header::type_id::create("hdr");
orig.hdr.hdr_type = 1;
$cast(copy, orig.clone()); // clone calls copy(), which calls do_copy()
copy.hdr.hdr_type = 99; // modifying copy's header...
// $display(orig.hdr.hdr_type) → prints 99, not 1!
// BECAUSE: `uvm_field_object does shallow copy — hdr pointer is shared!
// ✓ FIX: override do_copy() for deep copy of object fields ─────────────
function void do_copy(uvm_object rhs);
complex_txn rhs_cast;
super.do_copy(rhs); // copies scalar fields via field macros
$cast(rhs_cast, rhs);
// Manually deep-copy the object handle
hdr = header::type_id::create("hdr");
hdr.copy(rhs_cast.hdr); // new object with same field values
endfunction§10 — Bugs and Debugging
Bug 1 — UVM_NOCOMPARE Silently Making Scoreboard Always Pass
// ❌ WRONG — UVM_NOCOMPARE applied to critical data field ─────────────
class apb_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_NOCOMPARE) // ← data NOT compared!
`uvm_field_int(write, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
endclass
// In scoreboard:
// txn_actual.compare(txn_expected) → returns 1 even if data differs!
// → `uvm_error never fires → CI reports PASS → DUT corruption undetected
// ✓ CORRECT — use UVM_DEFAULT for fields that must be compared ────────
`uvm_field_int(data, UVM_DEFAULT) // ← compare enabled
// If you genuinely need to exclude from comparison, document it clearly:
// `uvm_field_int(timestamp, UVM_DEFAULT | UVM_NOCOMPARE) // timestamp: DUT-generated, not predicted
// And add an assertion in check_phase to verify the field separately if needed.Bug 2 — Missing begin/end Wrapper — Methods Not Generated
// ❌ WRONG — field macro outside the begin/end block ─────────────────
class bad_txn extends uvm_sequence_item;
`uvm_object_utils(bad_txn) // ← compact form, no begin/end
`uvm_field_int(addr, UVM_DEFAULT) // ← OUTSIDE the wrapper — ignored!
rand logic [31:0] addr;
endclass
// bad_txn t;
// t.print() → shows "bad_txn" with NO fields listed
// t.compare(other) → always returns 1 (no fields registered for compare)
// t.clone() → creates empty object (no fields copied)
// ✓ CORRECT — field macros inside begin/end ───────────────────────────
class good_txn extends uvm_sequence_item;
`uvm_object_utils_begin(good_txn) // ← _begin form required for field macros
`uvm_field_int(addr, UVM_DEFAULT) // ← INSIDE: generates code for all 6 methods
`uvm_object_utils_end // ← closes the block
rand logic [31:0] addr;
function new(string n="good_txn"); super.new(n); endfunction
endclass
// good_txn t;
// t.addr = 32'h4000;
// t.print() → shows addr = 'h4000 ← field visible
// t.compare(other) → actually compares addr fieldBug 3 — Performance Kill: Field Macros on High-Frequency Transactions
// ── Auto-generated (field macro): ~6 function calls per field ────────
class slow_txn extends uvm_sequence_item;
`uvm_object_utils_begin(slow_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr, data;
// convert2string() → uses generic recorder → slow for high-frequency use
endclass
// ── Hand-written: one $sformatf call ─────────────────────────────────
class fast_txn extends uvm_sequence_item;
`uvm_object_utils(fast_txn) // compact — no field macros
rand logic [31:0] addr, data;
function string convert2string();
return $sformatf("addr=0x%0h data=0x%0h", addr, data);
endfunction
function void do_copy(uvm_object rhs);
fast_txn rhs_t; super.do_copy(rhs);
$cast(rhs_t, rhs);
addr = rhs_t.addr;
data = rhs_t.data;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
fast_txn rhs_t;
if (!$cast(rhs_t, rhs)) return 0;
return super.do_compare(rhs, comparer) &&
(addr === rhs_t.addr) &&
(data === rhs_t.data);
endfunction
endclass
// Rule of thumb:
// ≤ 8 scalar fields + low transaction rate → use field macros (convenience wins)
// > 8 fields OR high transaction rate (> 10K/ms) → hand-write do_copy/do_compare/convert2string§11 — Ready-to-Run: Field Macros vs Manual Implementation
Side-by-side: the same APB transaction implemented with field macros and with hand-written do_copy() / do_compare() / convert2string(). Run both and compare the print() output and compare() behaviour. Ready to Run — Questa / VCS / Xcelium
// field_macro_demo.sv
// Compile: vlog -sv field_macro_demo.sv
// Run: vsim -c work.tb_fm_top +UVM_TESTNAME=fm_test -do "run -all; quit"
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── VERSION A: field macros ────────────────────────────────────────────
class apb_macro_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_macro_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(write, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string n="apb_macro_txn"); super.new(n); endfunction
endclass
// ── VERSION B: manual implementation ──────────────────────────────────
class apb_manual_txn extends uvm_sequence_item;
`uvm_object_utils(apb_manual_txn)
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string n="apb_manual_txn"); super.new(n); endfunction
function string convert2string();
return $sformatf("[APB] addr=0x%08h data=0x%08h write=%0b",
addr, data, write);
endfunction
function void do_copy(uvm_object rhs);
apb_manual_txn r;
super.do_copy(rhs);
$cast(r, rhs);
addr = r.addr;
data = r.data;
write = r.write;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
apb_manual_txn r;
if (!$cast(r, rhs)) return 0;
if (addr !== r.addr) comparer.compare_field("addr", addr, r.addr, 32);
if (data !== r.data) comparer.compare_field("data", data, r.data, 32);
if (write !== r.write) comparer.compare_field("write", write, r.write, 1);
return (addr === r.addr) && (data === r.data) && (write === r.write);
endfunction
endclass
// ── Test: exercises both versions ─────────────────────────────────────
class fm_test extends uvm_test;
`uvm_component_utils(fm_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
apb_macro_txn a1, a2;
apb_manual_txn m1, m2;
phase.raise_objection(this);
// ── Macro version ─────────────────────────────────────────────
a1 = apb_macro_txn::type_id::create("a1");
a1.addr = 32'h4000; a1.data = 32'hA5A5; a1.write = 1;
`uvm_info("TEST", "=== Field Macro Version ===", UVM_LOW)
`uvm_info("TEST", $sformatf("convert2string: %s", a1.convert2string()), UVM_LOW)
a1.print();
$cast(a2, a1.clone());
a2.data = 32'hBEEF; // a1.data unchanged (clone = deep copy of scalars)
`uvm_info("TEST", $sformatf("compare (should differ): %0d",
a1.compare(a2)), UVM_LOW) // expect 0
// ── Manual version ────────────────────────────────────────────
m1 = apb_manual_txn::type_id::create("m1");
m1.addr = 32'h4000; m1.data = 32'hA5A5; m1.write = 1;
`uvm_info("TEST", "=== Manual Version ===", UVM_LOW)
`uvm_info("TEST", $sformatf("convert2string: %s", m1.convert2string()), UVM_LOW)
$cast(m2, m1.clone());
m2.data = 32'hBEEF;
`uvm_info("TEST", $sformatf("compare (should differ): %0d",
m1.compare(m2)), UVM_LOW) // expect 0
phase.drop_objection(this);
endtask
endclass
module tb_fm_top;
initial run_test();
endmodule
// Expected output:
// TEST: === Field Macro Version ===
// TEST: convert2string: addr=4000 data=a5a5 write=1 (macro format)
// Name Type Size Value
// apb_macro_txn object -
// addr integral 32 'h4000
// data integral 32 'ha5a5
// write integral 1 'h1
// TEST: compare (should differ): 0 ← data=0xa5a5 vs data=0xbeef
//
// TEST: === Manual Version ===
// TEST: convert2string: [APB] addr=0x00004000 data=0xa5a5a5a5 write=1
// TEST: compare (should differ): 0 ← same result, more controlThe Bug Field Macros Are Best At Hiding
A field excluded from comparison does not announce itself. The scoreboard keeps reporting matches, the regression stays green, and the DUT bug the flag was masking ships.
// ================================================================
// nocompare_trap.sv — a mismatch that compare() reports as a match.
// Questa : vlog -sv -L uvm_1_2 nocompare_trap.sv
// vsim -c -L uvm_1_2 nocompare_trap_top -do "run -all; quit"
// VCS : vcs -sverilog -ntb_opts uvm-1.2 nocompare_trap.sv && ./simv
// Xcelium: xrun -sv -uvm nocompare_trap.sv
// ================================================================
`include "uvm_macros.svh"
import uvm_pkg::*;
class apb_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
// Added during bring-up so a noisy debug field stopped failing runs.
// It also silently removed the DUT's response code from every check.
`uvm_field_int(resp, UVM_DEFAULT | UVM_NOCOMPARE)
// Never registered at all — the most invisible exclusion there is.
`uvm_object_utils_end
rand bit [31:0] addr, data;
bit [1:0] resp;
bit parity_err; // ← not in any macro: not copied, not compared
function new(string n="apb_txn"); super.new(n); endfunction
endclass
class nocompare_trap_test extends uvm_test;
`uvm_component_utils(nocompare_trap_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
apb_txn expected, actual;
phase.raise_objection(this);
expected = apb_txn::type_id::create("expected");
actual = apb_txn::type_id::create("actual");
expected.addr = 32'h1000; expected.data = 32'hDEAD_BEEF;
expected.resp = 2'b00; expected.parity_err = 1'b0; // OKAY, clean
actual.addr = 32'h1000; actual.data = 32'hDEAD_BEEF;
actual.resp = 2'b11; actual.parity_err = 1'b1; // ERROR + parity!
// Two genuinely different transactions.
if (expected.compare(actual))
`uvm_info("TRAP", "compare() returned MATCH — resp and parity_err ignored",
UVM_LOW)
else
`uvm_error("TRAP", "compare() returned MISMATCH")
// copy() has the same blind spot, from the same registration.
begin
apb_txn c = apb_txn::type_id::create("c");
c.copy(actual);
`uvm_info("TRAP", $sformatf(
"after copy: resp=%0d (copied) parity_err=%0d (LOST — was %0d)",
c.resp, c.parity_err, actual.parity_err), UVM_LOW)
end
phase.drop_objection(this);
endtask
endclass
module nocompare_trap_top;
initial run_test("nocompare_trap_test");
endmodule
// Expected:
// UVM_INFO [TRAP] compare() returned MATCH — resp and parity_err ignored
// UVM_INFO [TRAP] after copy: resp=3 (copied) parity_err=0 (LOST — was 1)Two different exclusions, two different consequences. resp carries UVM_NOCOMPARE, so it is copied but never checked — a DUT returning an error response compares clean forever. parity_err was never registered at all, so it is neither compared nor copied: the scoreboard's stored copy silently loses it, and any later check against that copy sees a zero that was never observed.
The second is worse precisely because it is not a flag anybody chose. It is a field somebody added to the class and forgot to add to the macro block, and nothing in the language or the library connects the two.
A scoreboard reported 100% match while the DUT returned error responses on 4% of transfers
FIELD-MACRO-NOCOMPARE-MASKAn APB scoreboard had matched cleanly for months. A firmware team then reported that roughly 4% of their reads came back with error responses that the RTL should never have generated.
Re-running the exact failing sequence in the UVM environment produced a clean pass. The environment and the firmware disagreed about the same traffic.
`uvm_object_utils_begin(apb_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(resp, UVM_DEFAULT | UVM_NOCOMPARE) // ← added during bring-up
`uvm_object_utils_endGit blame put the UVM_NOCOMPARE at eleven months earlier, with the message "stop resp mismatches failing the smoke test". At the time the monitor was not yet sampling resp correctly, so every comparison failed on it. Excluding the field unblocked the suite, the monitor was fixed a week later, and the flag was never revisited.
The transactions were clearly different when printed, which was the moment the investigation turned:
expected: addr=0x00001000 data=0xdeadbeef resp=0
actual : addr=0x00001000 data=0xdeadbeef resp=3
UVM_INFO ... [SB] MATCHprint() showed the field because UVM_PRINT was still set — only compare() was blind to it. That asymmetry is the signature of a flag-level exclusion rather than a monitor or scoreboard bug, and it is visible in one log line if you print both objects on a passing comparison.
UVM_NOCOMPARE removes the field from the generated do_compare entirely. There is no report, no verbosity level that reveals it, and no difference between "these transactions matched" and "these transactions matched on the fields I was allowed to look at."
The deeper cause is that the exclusion was a debugging workaround that outlived its reason. It was correct for one week and wrong for eleven months, and nothing in the code recorded which week it was for.
Remove the flag and let the field be checked:
`uvm_field_int(resp, UVM_DEFAULT)Then close the class of bug rather than the instance. Three guards are worth having, in increasing order of value.
Comment every exclusion with a reason and an owner — // NOCOMPARE: DUT returns X during reset, see JIRA-1234 — so the next reader can tell a deliberate policy from an expired workaround. Add a CI grep for UVM_NO flags that flags any occurrence without an adjacent justification comment.
And most usefully, write a negative self-test for the transaction itself: build two objects that differ in exactly one field, assert compare() returns 0, and repeat per field. It is a dozen lines, it runs in milliseconds, and it fails the moment a field stops participating — whether by an added flag or by someone forgetting to register a new field at all. That last case is the one no amount of flag review catches.
Every stored transaction in the scoreboard held the same payload — the most recent one
FIELD-OBJECT-SHALLOW-COPYAn AXI scoreboard stored each observed write transaction and compared it against the read-back later. With one transaction in flight it was perfect. Above about four outstanding, expected-value mismatches appeared, and the expected payloads were always wrong in the same direction — they matched the most recently observed transaction rather than the one under check.
The mismatch rate rose with outstanding depth, which sent the investigation into the reordering logic for two days.
class axi_payload extends uvm_object;
`uvm_object_utils_begin(axi_payload)
`uvm_field_array_int(bytes, UVM_DEFAULT)
`uvm_object_utils_end
byte bytes[];
endclass
class axi_txn extends uvm_sequence_item;
`uvm_object_utils_begin(axi_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_object(payload, UVM_DEFAULT) // ← default policy is SHALLOW
`uvm_object_utils_end
rand bit [31:0] addr;
axi_payload payload;
endclass
// In the monitor: one payload object, reused across transactions
function void sample();
txn = axi_txn::type_id::create("txn");
txn.addr = observed_addr;
txn.payload = m_payload; // same handle every time
ap.write(txn);
endfunctionPrinting the stored transactions at the point of mismatch showed distinct addresses and identical payloads:
stored[0]: addr=0x1000 payload=@331 bytes={DE AD BE EF}
stored[1]: addr=0x2000 payload=@331 bytes={DE AD BE EF}
stored[2]: addr=0x3000 payload=@331 bytes={DE AD BE EF}The @331 is the decisive detail. Three "different" stored transactions referenced one object. The addresses differed because addr is an integral field that was genuinely copied; the payload did not, because it is a handle.
uvm_field_object's default copy policy is shallow: the generated do_copy assigns the handle, not the object. Every stored transaction therefore pointed at the monitor's single reusable payload object, and each new sample overwrote the contents that all previously-stored transactions were sharing.
Two decisions combined to produce it, and neither is wrong alone. The monitor reusing one payload object is a normal allocation optimisation. The UVM_DEFAULT flag on an object field is the ordinary thing to write. Together they mean the scoreboard's history is one object wearing many addresses.
The depth-dependent symptom follows directly: with one transaction in flight, the store and the check happen before the next sample overwrites anything, so nothing is visibly wrong.
Make the copy deep, at whichever end you control. In the transaction, request the deep-copy policy on the object field:
`uvm_field_object(payload, UVM_DEFAULT | UVM_DEEP)Or, more robustly, have the producer stop sharing the handle at all:
txn.payload = axi_payload::type_id::create("payload");
txn.payload.copy(m_payload); // or: $cast(txn.payload, m_payload.clone())The second is preferable because it does not depend on every future consumer remembering a flag, and because a scoreboard should generally clone() anything it intends to keep — storing a handle to an object somebody else still owns is the general form of this bug, independent of field macros.
The guard that catches it early is a uniqueness assertion in the scoreboard: when storing, check that the incoming sub-object handle is not already present in the store. It is a few lines, it fires on the first duplicate rather than at the first depth-dependent mismatch, and it names the sharing directly instead of presenting as a reordering problem. do_copy and do_compare develops the same failure from the manual-implementation side, and Object Cloning covers clone() semantics.
Questions Engineers Actually Ask
They are the fastest way, not automatically the right one.
For a transaction that is a flat bag of integral fields with no conditional semantics, they are excellent: one line per field replaces thirty lines of hand-written do_copy and do_compare, and there is nothing to get wrong.
They stop fitting as soon as the semantics are conditional — comparing a sub-range, ignoring a field when a mode bit is set, treating an X as a match, or comparing floating-point values with a tolerance. None of that is expressible in a flag, and the usual failure is to contort the transaction to suit the macros rather than write the eight lines of do_compare that say what you mean.
A reasonable default: macros for VIP transactions that stay simple, manual do_* the moment a comparison needs an if.
Nothing visible, which is what makes it dangerous.
The field is not copied, not compared, not printed, not packed. It compiles, it simulates, and every scoreboard check that relies on it silently passes. A stored copy of the transaction will hold the field's default value rather than the observed one, so a later comparison against that copy is checking a value that was never sampled.
There is no diagnostic for this — the macros cannot know about a field they were not told about. The only reliable guard is a per-field negative self-test, as described in DebugLab 1: two objects differing in exactly one field must fail compare().
Where This Is Specified
- IEEE 1800.2-2020 (UVM) — field automation macros. The
uvm_field_int,uvm_field_enum,uvm_field_object,uvm_field_array_*,uvm_field_queue_*anduvm_field_aa_*families, and theuvm_object_utils_begin/uvm_object_utils_endblock they must appear inside. - IEEE 1800.2-2020 — field flag definitions.
UVM_COPY/UVM_NOCOPY,UVM_COMPARE/UVM_NOCOMPARE,UVM_PRINT/UVM_NOPRINT,UVM_RECORD/UVM_NORECORD,UVM_PACK/UVM_NOPACKas individually-numbered bit positions, and the compositeUVM_DEFAULT. - IEEE 1800.2-2020 —
uvm_objectautomation. How the generateddo_copy,do_compare,do_print,do_pack,do_unpackanddo_recordimplementations consume the registered field list. - IEEE 1800.2-2020 —
uvm_comparer.compare_fieldand the accumulation of miscompares that a macro-generated comparison performs on your behalf. - IEEE 1800.2-2020 — object copy policies. The shallow default for
uvm_field_objectand the deep-copy policy flag. - IEEE 1800-2023 §11.4.7 — Logical operators. Short-circuit evaluation of
&&, and why manual comparison accumulators use&=.
Related Reading
- do_copy and do_compare — the manual alternative in full: the
supercall,$castguarding,compare_field, and the shallow-copy aliasing failure. - Object Cloning —
clone()and deep copy, the fix for a shared sub-object handle. - UVM Object — the base class whose automation these macros generate.
- Sequence Items and Transaction Objects — where field macros are most often applied.
- Data Checking and Scoreboard Debugging — what an excluded field does to a comparison downstream.
- The UVM Factory — the other half of what
uvm_object_utilsprovides: registration for factory creation and override.
§12 — Interview Questions
Beginner Level
Intermediate Level
Senior / Architect Level
§13 — Best Practices
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Always use uvm_object_utils_begin/end` when adding field macros — never uvm_object_utils` (compact form) | Field macros outside the begin/end block are silently ignored — print/compare/copy all appear to work but process no fields |
| BP-2 | Use UVM_DEFAULT for all primary protocol fields (addr, data, write, etc.) that must be compared | UVM_NOCOMPARE on a primary field silently disables scoreboard checking — one of the hardest bugs to find |
| BP-3 | Explicitly use UVM_DEFAULT | UVM_NOCOMPARE for fields that should not be compared — and add a comment explaining why | Documents intent; prevents future engineers from "fixing" the flag and breaking intentional exclusions |
| BP-4 | Never rely on ``uvm_field_object` for deep copy — always override do_copy() | Shallow copy via field macros creates shared object handles; modifying one modifies all, causing silent data corruption |
| BP-5 | For transactions with > 8 fields or used in tight loops, hand-write convert2string() | The macro-generated version goes through the recorder machinery; a direct $sformatf is 2–5× faster for high-frequency monitoring |
| BP-6 | Declare field macros in the same order as physical wire layout when using pack/unpack | Pack order follows declaration order; a different order from the wire protocol requires a manual do_pack() |
| BP-7 | Test your compare() by creating two objects with known differences and calling compare() — verify it returns 0 | A comparison that always returns 1 is worse than no comparison — it gives false confidence; always verify the mechanism works |
| BP-8 | Prefer hand-written do_compare() for production scoreboard transactions in shared VIPs | Explicit comparison logic is auditable; field macros hide the comparison logic behind flags that require understanding of macro internals to audit |
§14 — Summary
| Aspect | Field Macros | Manual Implementation |
|---|---|---|
| Setup effort | One line per field — minimal | Write do_copy, do_compare, convert2string — 20–40 lines |
| Object handle fields | Shallow copy — dangerous for nested objects | Full control — explicit deep copy in do_copy() |
| Comparison logic | Per-field flags only — no conditional comparisons | Any logic — compare field A only when field B has a specific value |
| Performance (high freq) | Overhead from recorder and format machinery | Direct $sformatf and === comparison — fastest possible |
| Pack/unpack order | Follows declaration order — may not match wire layout | Any bit layout expressible in do_pack() |
| Auditability | Requires understanding flag combinations to audit | Fully explicit — every field and condition visible |
| Best used for | Quick prototyping; transactions with ≤ 8 scalar fields at low transaction rate | Production VIPs; high-frequency monitors; complex nested objects |