Printer & Comparer Policies
Object Utility Methods covered print() and compare() as capabilities every uvm_object gets, with do_print(uvm_printer printer) and do_compare(uvm_object rhs, uvm_comparer comparer) as their hooks — both taking a policy object as an argument, without exploring what that policy object actually configures. This page covers both: uvm_printer and uvm_comparer, and a couple of sharp edges worth knowing before relying on either in a real testbench.
uvm_printer: three built-in styles
fifo_txn txn = fifo_txn::type_id::create("txn");
txn.op = fifo_txn::WRITE;
txn.wr_data = 8'hA5;
txn.print(); // uses the default printer (table style)
txn.print(uvm_default_tree_printer); // indented tree instead of a table
txn.print(uvm_default_line_printer); // everything on one line
Three printer singletons ship with UVM, each producing a different layout from the exact same underlying field data: uvm_default_table_printer (the default — a formatted table with name/type/size/value columns), uvm_default_tree_printer (indented hierarchy, useful for deeply nested objects), and uvm_default_line_printer (compact single-line output, useful inside a `uvm_info call where a multi-line table would be unwieldy). Calling print() with no argument, or explicitly with null, falls back to uvm_default_printer silently — never an error.
Configuring layout via knobs
Every printer exposes a knobs object controlling its output:
uvm_default_table_printer.knobs.depth = 2; // stop recursing into nested objects past 2 levels
uvm_default_table_printer.knobs.size = 0; // hide the "size" column
uvm_default_table_printer.knobs.hex_radix = "0x"; // print hex as 0x.. instead of 'h..
uvm_default_table_printer.knobs.indent = 4; // spaces per nesting level (tree printer)
These are the knobs reached for most often: depth caps recursion (useful for a deeply nested environment object where only the top couple of levels matter for a given debug session), size toggles the size column off for a cleaner table, hex_radix matches house style for hex formatting, and indent controls tree-printer spacing.
Overriding do_print() alongside macro automation
Object Utility Methods showed do_print() overridden by hand as an alternative to macro automation — the two aren't mutually exclusive. A hand-written do_print() can call super.do_print(printer) first (letting any macro-generated fields print normally) and then append extra fields of its own:
function void fifo_txn::do_print(uvm_printer printer);
super.do_print(printer);
printer.print_string("summary", $sformatf("%s data=%0h", op.name(), wr_data));
endfunction
For a field that's itself a nested uvm_object (not the case for fifo_txn, but common in larger transaction classes), do_print() should call printer.print_object() on it rather than a raw field-print method, so the printer recurses into it correctly and respects depth.
sprint(): the same printer policy, returned as a string instead of displayed
`uvm_info("TXN", txn.sprint(uvm_default_line_printer), UVM_MEDIUM)
sprint() takes the exact same printer argument as print() — any of the three built-in singletons, or a custom one with its own knobs — but returns the formatted output as a string instead of sending it straight to $display. This matters anywhere the output needs to be embedded inside something else rather than shown standalone, most commonly wrapping it in a `uvm_info call (as above) so a transaction dump goes through UVM's own verbosity/reporting filtering instead of printing unconditionally. print() itself is effectively $display(sprint()) under the hood — the same policy object, just two different endpoints for the resulting text. More precisely, print() writes sprint()'s output via $fwrite(knobs.mcd, sprint()), where knobs.mcd (multi-channel descriptor) defaults to standard output — the same effect as $display unless mcd is repointed at a file descriptor opened with $fopen(), letting a transaction dump go to a log file instead of the simulator console without touching any of the printing code itself.
uvm_comparer: a policy object, not built into the objects being compared
uvm_comparer cmp = new();
cmp.show_max = 5; // stop listing miscompares after 5
cmp.verbosity = UVM_LOW;
if (!txn_a.compare(txn_b, cmp))
`uvm_error("CMP", "Transactions differ")
show_max caps how many individual field mismatches get logged (useful when two large objects disagree in many places at once and a full dump would be noise), and verbosity sets the reporting detail for each one. Under the hood, field-level comparisons happen through methods like compare_field(), compare_field_int(), compare_object(), and compare_string() — the same methods a hand-written do_compare() would call directly, as shown in Object Utility Methods.
compare_field_int() is capped at 64 bitsFields wider than 64 bits can't go through compare_field_int() — it needs the more general compare_field() instead. This rarely bites a small transaction like fifo_txn, but is worth remembering the moment a transaction class grows a wide field (a cache line, a burst payload) and compare_field_int() starts silently truncating or erroring rather than comparing the whole value.
A uvm_comparer's internal miscompare count is not automatically reset between separate compare() calls if the comparer object itself is reused across them — calling compare() through uvm_object::compare() (as in the example above) resets it automatically first, but bypassing that entry point to call comparison methods more directly does not. Reusing one uvm_comparer instance across an entire test run's worth of unrelated comparisons, expecting each one to start fresh, is a subtle way to get a "still failing" result that's actually reporting a stale miscompare from several comparisons ago.
What's next
Printer and comparer are configuration objects for capabilities every uvm_object already has. The final page in this section covers two mechanisms for extending behavior after the fact — callbacks, and the event/event-pool classes used for cross-component synchronization.