UVM
Common Factory Bugs
The five recurring factory failure modes — created with new, override registered too late, override not a derivative, missing registration, instance-path mismatch — each with its distinct symptom and fix.
UVM Factory · Module 6 · Page 6.6
The Engineering Problem
You now know the factory mechanism (Modules 6.1–6.3), how type and instance overrides work (6.4), and the debug tooling — print_topology, factory print(), the override-chain dump — for inspecting it (6.5). But there's a gap between having the tools and knowing what you're looking at. Factory bugs are not random: across every team and every project, the same handful of failure modes recur. An override that silently does nothing. A create that fails outright. An override that's rejected by the factory. A component that builds as the wrong type at one path but the right type everywhere else.
The problem this chapter solves is recognition. If you know the taxonomy — the small set of factory failure modes and the distinct signature each one produces — you diagnose in seconds instead of rediscovering the bug from scratch each time. There are essentially five: created with new (the override has nothing to hook into), override registered too late (after the create already resolved), override not a derivative (the factory rejects it), missing registration (the type can't be created at all), and instance-path mismatch (the override targets a path that doesn't exist, so it never applies). Each has a clean symptom→cause→fix. This chapter is that taxonomy — the usual suspects, how to tell them apart, and how to fix each.
What are the recurring UVM factory bugs — created with
new, override too late, override not a derivative, missing registration, instance-path mismatch — what distinct symptom does each produce, and how do you tell them apart and fix them?
Motivation — why these specific bugs recur
The factory's power — indirection between requesting a type and constructing it (Module 6.2) — is exactly what makes its bugs subtle. Each common bug is a way that indirection gets broken:
- The indirection gets bypassed (
new). If construction doesn't go through the factory, there's no resolution step, so overrides have nothing to act on. The bug is silent because the code compiles and runs — you just get the base type. This is the single most common factory bug. - The indirection runs before the override is registered (too late). The factory resolves each
createat the moment it's called (build time). If the override is registered after that moment, the create already resolved to the base type. Also silent, also "I get the base type" — but the cause is timing, not bypass. - The override is type-incompatible (not a derivative). The factory will only substitute a type that is-a the original (extends it), because the handle that receives it is of the original type. An incompatible override is rejected — this one is loud (a factory error), not silent.
- The type was never registered (missing
uvm_*_utils). Without registration there's no proxy, socreateitself fails — the type can't be constructed at all. Loud, at the create site. - The override targets a path that doesn't exist (instance mismatch). An instance override applies only where the hierarchical path matches; a typo or wrong wildcard means the path never matches, so the override silently doesn't apply there while possibly applying elsewhere.
The motivation, in one line: the factory's bugs cluster into "the indirection was bypassed / mistimed / type-rejected / never-registered / mis-targeted" — five failure modes, three silent and two loud, and knowing which signature you're seeing tells you immediately which one you have.
Mental Model
Hold the factory bugs as a line-up of five usual suspects:
When something factory-related goes wrong, it's almost always one of five usual suspects — and each leaves a different fingerprint. Two of them produce the same visible crime — "my override did nothing, I got the base type" — but for different reasons: Bypass (you built it with
new, so the factory never saw the request) and Too-Late (you registered the override after the build already happened). Two are loud and announce themselves: Wrong-Family (your override type doesn't extend the original, so the factory refuses it) and Unregistered (you never putuvm_*_utilson the class, socreatecan't make it at all). And one is selective: Wrong-Address (your instance override names a path that doesn't exist, so it applies nowhere — or somewhere you didn't mean). Diagnosis is a line-up: read the fingerprint — silent vs loud, at-create vs at-override, everywhere vs one-path — and you've named the suspect.**
So when a factory thing misbehaves, don't start from zero — run the line-up. Silent "got the base type"? It's Bypass or Too-Late (check: was it new, or was the override set after the create?). Loud error? It's Wrong-Family or Unregistered (check: is the override a subclass, and is the class registered?). Works most places but not one path? It's Wrong-Address (check the instance path string).
Visual Explanation — the diagnostic decision tree
The fastest way to use the taxonomy is as a decision tree: read the symptom, branch, and land on the suspect. The single most useful split is silent-vs-loud, then sub-divide each.
The tree turns five separate bugs into one diagnostic flow. The first and most valuable split is loud vs silent, because it halves the search instantly. Loud (the factory printed an error) means construction itself broke: either the factory rejected an override (the override type isn't a subclass of the original — Wrong-Family) or create failed because the type was never registered (Unregistered). Silent (no error, but you got the wrong type) means the factory ran fine and just produced something you didn't intend — so the next split is where: if the wrong type appears everywhere the type is built, it's a global cause — either Bypass (new instead of create) or Too-Late (the override was registered after the create resolved); if the wrong type appears at one specific path only, it's an instance-path mismatch (the override targeted the wrong path). Notice the two silent-everywhere leaves are the ones that look identical in behaviour — both give "base type instead of override" — and the diagram separates them by the only distinguishing question that matters: was it built with new, or was the override set too late? That question is the heart of factory debugging, and the next sections drill into it.
RTL / Simulation Perspective — the five bugs in code
Each failure mode is a small, recognizable code shape. Seeing them side by side trains the recognition.
// ── BUG 1: BYPASS — new instead of type_id::create (silent; override ignored) ──
drv = new("drv", this); // ✗ bypasses the factory entirely
drv = my_driver::type_id::create("drv", this); // ✓ goes through the factory
// ── BUG 2: TOO LATE — override registered after the create resolved (silent) ──
function void my_test::build_phase(uvm_phase phase);
super.build_phase(phase); // ✗ builds env → env's create() resolves NOW
set_type_override_by_type(my_driver::get_type(), err_driver::get_type()); // too late
endfunction
// ✓ fix: register the override BEFORE the create — set it first, then super.build_phase()
// ── BUG 3: WRONG FAMILY — override type isn't a derivative (LOUD: rejected) ──
class err_driver extends uvm_component; // ✗ does NOT extend my_driver
set_type_override_by_type(my_driver::get_type(), err_driver::get_type()); // factory rejects
class err_driver extends my_driver; // ✓ is-a my_driver → factory accepts
// ── BUG 4: UNREGISTERED — missing uvm_*_utils (LOUD: create fails) ──
class my_driver extends uvm_driver#(my_item);
// `uvm_component_utils(my_driver) // ✗ missing → no proxy → create fails
endclass
// ── BUG 5: WRONG ADDRESS — instance-override path doesn't match (silent, one path) ──
set_inst_override_by_type("env.agnt.drv", // ✗ real path is "env.agent.drv" (typo) → never applies
my_driver::get_type(), err_driver::get_type());Read the shapes. Bypass is a new (or a direct constructor call) where a type_id::create belongs — the giveaway is the absence of ::type_id::create. Too-Late is an override registered after the construction it was meant to affect already ran — classically, set_type_override* placed after super.build_phase() in a test (so the env it overrides has already been built and its children already created). Wrong-Family is an override whose type doesn't extend the original — the factory rejects it because the original-typed handle couldn't hold it. Unregistered is a class missing its uvm_component_utils/uvm_object_utils macro — create has no proxy to build from. Wrong-Address is an instance override whose path string doesn't match the actual hierarchical path — a typo or wrong wildcard, so the override applies nowhere it was intended. Three are silent (Bypass, Too-Late, Wrong-Address), two are loud (Wrong-Family, Unregistered) — exactly the decision-tree split.
Verification Perspective — the two that look identical
The hardest pair to separate is Bypass and Too-Late, because they produce the same observable result: the base type instead of the override, with no error. The distinguishing test is mechanical, and worth internalizing.
This is the diagnostic that separates the look-alikes. Both Bypass and Too-Late present as "the override did nothing, I got the base type, and there was no error" — so behaviour alone can't tell them apart. The separation is two mechanical checks. First: how was the component constructed? If the construction site used new (or a direct constructor), it's Bypass — the create request never reached the factory, so the override had nothing to act on; the fix is at the site (switch to type_id::create). If the site correctly used type_id::create, move to the second check: when was the override registered relative to that create? If the override was set after the create resolved (the classic case: set_type_override* after super.build_phase() built the env), it's Too-Late — the create already produced the base type before the override existed; the fix is ordering (register the override before the create, i.e., before super.build_phase()). Only if construction used create and the override was registered before that create should the override actually apply. Internalizing these two checks — site, then timing — is the single highest-value factory-debug skill, because it resolves the most common and most confusing factory bug.
Runtime / Execution Flow — why timing decides the Too-Late bug
The Too-Late bug is purely about when things happen during build_phase, top-down. Seeing the runtime order explains exactly why an override placed after super.build_phase() misses.
The runtime order is the whole explanation. build_phase executes top-down (Module 5.2): the test's build_phase runs before its children are built. The moment the test calls super.build_phase() (or otherwise triggers building the environment), the env and everything under it — including the driver you wanted to override — are created right then, each create resolving against whatever overrides exist at that instant. So there is exactly one safe window to register a type override: before that creation happens — i.e., before super.build_phase() in the test. Register it there, and when the driver's create resolves moments later, the override is already in the factory and applies. Register it after super.build_phase(), and you've missed the train: the driver's create already resolved to the base type, and your override — though now correctly in the factory — affects nothing that was already built. This is why Too-Late is a timing bug, not a correctness bug: the override is perfectly valid; it just arrived after the create it was meant to influence. The rule that prevents it: register overrides before the construction they target — for a test, before super.build_phase().
Waveform Perspective — Too-Late on a timeline
The Too-Late bug is a timing relationship, so a timeline makes it vivid: the create resolves at one instant, and an override registered after that instant simply comes too late.
Too-Late: the override is registered after the create has already resolved
10 cyclesThe waveform isolates the one variable that matters: order. In the GOOD case (left), ovr_set pulses first — the override is registered into the factory — and only then does create_resolve pulse, so the driver's create finds the override and resolved_type becomes ERR (overridden). In the BAD case (right), the order is reversed: create_resolve pulses first, so the create resolves against an empty override table and resolved_type becomes BASE; the ovr_set that pulses afterward is correct but late — the create it needed to influence has already resolved, so resolved_type stays BASE. Both cases register the identical override and run the identical create; only the order differs, and that order is the entire bug. This is why Too-Late is invisible to a code reviewer scanning for "is the override correct?" — the override is correct; it's mis-ordered — and why the fix is never "fix the override" but always "move the override earlier than the create."
DebugLab — the override that was registered too late
An override that was correct in every way — except it ran after the create
An engineer wrote a test to inject an error driver: set_type_override_by_type(my_driver::get_type(), err_driver::get_type()). The override type was correct (err_driver extends my_driver), the components were all created with type_id::create, and there were no errors of any kind — the simulation ran clean. But the error driver never appeared: every transaction came from the normal my_driver, and print_topology showed drv as my_driver, not err_driver. The override seemed to do nothing, silently.
The override was registered after super.build_phase(), so the driver had already been created — and resolved to the base type — before the override existed:
test::build_phase:
super.build_phase(phase); // ① env built → drv created NOW → resolves to my_driver
set_type_override_by_type(my_driver::get_type(), // ② override registered — but drv already exists
err_driver::get_type());
result: drv is my_driver (created at ①, before the override at ② existed)
no error — the override is valid, it just came too late to affect ①
fix: register the override BEFORE super.build_phase():
set_type_override_by_type(my_driver::get_type(), err_driver::get_type()); // ① override in place
super.build_phase(phase); // ② drv created → resolves to err_driverThis is the Too-Late suspect from the taxonomy. build_phase is top-down: when the test calls super.build_phase(), the environment and its driver are created immediately, each create resolving against the overrides registered at that instant — and at that instant, no override existed. The set_type_override* call on the next line registered a perfectly valid override, but into a factory whose relevant create had already happened. Nothing was wrong with the override; it was simply ordered after the construction it was meant to change. The fix is to move the override registration before super.build_phase(), so it's in the factory when the driver's create resolves. (If the test builds children explicitly rather than via super.build_phase(), the same rule applies: the override must precede that construction.)
The tell is a silent, valid override that applies to nothing — and print_topology confirming the base type was built. Diagnose it:
- Confirm it's silent, not loud. No factory error means the override wasn't rejected (so it's a derivative — not Wrong-Family) and
createdidn't fail (so it's registered — not Unregistered). Silent narrows you to Bypass, Too-Late, or Wrong-Address. - Check the construction site. Confirm the component was created with
type_id::create, notnew. Ifnew, it's Bypass, not Too-Late. (Here it wascreate— so not Bypass.) - Check the override timing. Find where the override is registered relative to the construction it targets. An override after
super.build_phase()(or after the explicit child create) is the Too-Late signature. - Confirm with topology.
print_topologyshowing the base type (not the override) at a path where the override should apply everywhere — not just one path — distinguishes Too-Late from a Wrong-Address instance mismatch.
Register overrides before the construction they target:
- Set type overrides before
super.build_phase(). In a test, register all factory overrides at the top ofbuild_phase, before callingsuper.build_phase()— so they're in the factory when the env and its children are created. - Treat overrides as setup, not afterthought. An override is configuration that must exist before construction. Place it with the same discipline as config_db settings the children will read — early, before the children are built.
- Verify with
print_topology. After adding an override, confirm viaprint_topology(or+UVM_TESTNAMErun with factory print) that the overridden type actually appears in the tree — don't assume a clean run means the override applied.
The one-sentence lesson: a factory override only affects constructions that happen after it is registered, so a valid override that silently does nothing was almost certainly registered too late — after the targeted create (classically, after super.build_phase()) — and the fix is to move it earlier, not to change the override.
Common Mistakes
- Constructing with
newinstead oftype_id::create. Bypasses the factory, so overrides are silently ignored — the most common factory bug. Always create through the factory. - Registering an override after
super.build_phase(). The targetedcreatealready resolved to the base type; the override comes too late. Register overrides beforesuper.build_phase(). - Making the override type not a derivative of the original. The factory rejects an override whose type doesn't
extendthe original (the original-typed handle can't hold it). The override must be a subclass. - Forgetting the
uvm_*_utilsregistration. No proxy, socreatefails outright — the type can't be built at all. Register every factory-created class. - Mistyping the instance-override path. An instance override only applies where the path matches; a typo or wrong wildcard means it silently applies nowhere it was intended. Match the path to the real topology (
print_topology). - Assuming a clean run means the override applied. Bypass and Too-Late are both silent — verify with
print_topologythat the overridden type is actually in the tree.
Senior Design Review Notes
Interview Insights
There are essentially five recurring failure modes. First, constructing with new instead of type_id::create — this bypasses the factory entirely, so any override is silently ignored and you get the base type; it's the single most common factory bug. Second, registering an override too late — after the create it was meant to affect already resolved (classically, placing set_type_override* after super.build_phase() in a test, by which point the env and its driver are already built); the override is valid but applies to nothing, again silently. Third, an override type that isn't a derivative of the original — the factory rejects it loudly, because the original-typed handle can't hold a type that doesn't extend the original. Fourth, a missing uvm_component_utils/uvm_object_utils registration — there's no proxy, so create itself fails loudly, the type can't be built at all. Fifth, an instance-override path that doesn't match the real hierarchy — a typo or wrong wildcard means the override silently applies nowhere it was intended. The useful way to hold these is by signature: three are silent (new, too-late, path-mismatch) and two are loud (not-a-derivative, missing-registration), and within the silent ones, "everywhere" points to new or too-late while "one path" points to a path mismatch.
Exercises
- Run the line-up. For each symptom — (a) loud "type not registered," (b) silent base-type everywhere, (c) loud "override rejected," (d) silent base-type at one path only — name the suspect and the fix.
- Separate the look-alikes. Given a silent "got the base type" bug, write the two checks (site, then timing) that distinguish Bypass from Too-Late, and the fix for each.
- Fix the ordering. A test calls
super.build_phase()thenset_type_override*. Rewrite it so the override applies, and explain in one sentence why the original missed. - Verify, don't assume. Describe how you'd confirm an override actually took effect after a clean simulation run, and why a clean run alone isn't proof.
Summary
- Almost every factory bug is one of five recognizable failure modes: Bypass (
newinstead ofcreate— silent), Too-Late (override registered after thecreateresolved — silent), Wrong-Family (override not a derivative — loud, rejected), Unregistered (missinguvm_*_utils— loud, create fails), and Wrong-Address (instance-path mismatch — silent, one path). - The fastest diagnosis is a decision tree: loud vs silent splits construction failures (rejected / failed create) from wrong-type-produced; then where (everywhere vs one path) and how (new vs too-late) names the exact suspect.
- The two silent look-alikes — Bypass and Too-Late — produce identical behaviour and are separated by two mechanical checks: the construction site (
newvscreate), then the override timing (before vs after the create). - Too-Late is a timing bug, not a correctness bug: because
build_phaseis top-down and eachcreateresolves against the overrides present at that instant, an override registered aftersuper.build_phase()misses — the fix is to register it before. - The durable rule of thumb: construct through the factory (
type_id::create, nevernew), register overrides before the construction they target (beforesuper.build_phase()), make the override type a derivative of the original, register every class withuvm_*_utils, and verify withprint_topology— because three of the five factory bugs are silent and a clean run is not proof the override applied.
Next — Override Best Practices: the taxonomy of bugs leads directly to the disciplines that prevent them — where to register overrides, type vs instance, how to keep override chains readable, and the conventions that keep a factory-driven environment predictable as it scales.