Base Classes & the Class Hierarchy
hello_env and hello_test from the previous page both extended UVM base classes without much explanation of where those classes actually come from. Nearly every class in a UVM testbench — driver, monitor, sequence, transaction, test — descends from one of two root branches, and knowing which branch a class sits on tells you most of what you need to know about how it behaves before you've read a line of its documentation.
The two branches
uvm_voidsits at the very top as a bare, empty anchor class — it exists mainly so generic containers and utilities can hold "any UVM type" without depending onuvm_objectspecifically. You'll almost never extend it directly; it's worth knowing the name exists mainly so it doesn't look like a typo the first time it shows up in a class reference.uvm_objectis the base for anything that's created, used, and discarded during simulation, with no fixed position in the testbench hierarchy — this is the object-side branch covered in more depth on the next page.uvm_componentis the base for anything built once and permanent for the life of the simulation, and it's the branch that participates in phasing (see Phases & Objections).
Why reporting lives above uvm_component, not inside it
One detail in that diagram is easy to miss but explains something that otherwise looks inconsistent: `uvm_info/ `uvm_error and their relatives are defined on uvm_report_object, which sits between uvm_object and uvm_component — not on uvm_component itself.
That placement is deliberate. Sequences aren't components (they're uvm_sequence_item-branch objects, transient and hierarchy-free), yet sequences clearly need to report errors too — a sequence that can't call `uvm_error when randomization fails would be a real gap. Because uvm_sequence actually inherits from uvm_object → ... eventually reaching classes that mix in reporting capability independently of the component tree, both branches get full reporting without uvm_component needing to be a prerequisite for it. The practical takeaway: don't assume something needs to be a component just because it needs to log messages.
uvm_transaction sits between uvm_object and uvm_sequence_item in the diagram above for historical completeness, but extending it directly is discouraged — it predates uvm_sequence_item and lacks the sequencer-handshake plumbing every real transaction needs. In practice, every transaction class in this section (and in any UVM testbench you'll read) extends uvm_sequence_item, never uvm_transaction directly.
uvm_root: the tree's implicit top
Every component tree in this section has been drawn starting from a test — but the test itself has a parent too. uvm_root is a singleton (a class with exactly one instance, automatically created — not something you construct yourself) that sits above every test, auto-created the moment run_test() is called. It's accessible globally through the variable uvm_top, or via uvm_root::get().
task run_phase(uvm_phase phase);
// Find every component whose name ends in "driver", anywhere in the tree
uvm_component drivers[$];
uvm_top.find_all("*driver", drivers);
foreach (drivers[i])
`uvm_info("SCAN", drivers[i].get_full_name(), UVM_LOW)
endtask
uvm_top exposes a handful of tree-wide operations worth knowing exist even before you need them:
find(pattern)/find_all(pattern)— locate one or all components matching a wildcard path (*for any characters,?for exactly one), searching the entire tree from the top.set_report_verbosity_level_hier(level)— set a verbosity level on every component in the tree at once, rather than one at a time (see Reporting & Messaging for what verbosity levels do).print_topology()— the same method used in Getting Started's hello-world example, callable on any subtree, not just the full top.
Navigating the tree from any component
Every uvm_component — not just uvm_root — carries its own hierarchy-navigation API, useful for anything that needs to inspect its surroundings rather than assume a fixed structure:
uvm_component parent = get_parent();
uvm_component kids[$];
get_children(kids);
uvm_component one_kid = get_child("driver");
uvm_component nested = lookup("agent.driver"); // full dotted path required
string full_path = get_full_name();
int tree_depth = get_depth();
lookup() needs a full path, and this API needs a built treelookup("driver") from an environment will not find a driver nested two levels down inside an agent — it needs the full dotted path from the calling component's own position: lookup("agent.driver"). Separately, none of these methods are meaningful until build_phase has finished running across the whole tree — calling them from inside build_phase itself risks looking up children that don't exist yet. connect_phase onward is safe.
A gotcha worth internalizing early: parent = null
Every uvm_component constructor takes a parent argument (uvm_object's doesn't — see Object Utility Methods for that distinction). Passing null doesn't leave a component parentless; it silently reparents it to uvm_top instead. That's occasionally exactly what's wanted (uvm_root itself is the obvious case), but as an accidental typo — passing null where this was meant — it produces a component that still runs, still shows up in print_topology(), but sits in the wrong place in the hierarchy with no error raised anywhere. If a component's name or configuration is behaving unexpectedly, checking where it actually landed in the printed topology is often faster than re-reading the constructor call.
What's next
With the two branches and the tree they attach to now in view, the next page covers what uvm_object specifically gives every object for free — printing, copying, comparing, and serializing — before Utility & Field Macros covers the macros that automate all of it.