Skip to content

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 create at 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, so create itself 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 put uvm_*_utils on the class, so create can'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.

Decision tree mapping factory-bug symptoms to the five failure modesloudsilentrejectedfailed createeverywhereone pathnewtoo lateFactory notbehaving asexpectedLoud (an error is reported) or silent (wrong type, no error)?Loud (anerror isreported) orsilent (wron…Error: rejected override, or failed create?Error:rejectedoverride, orfailed…Override REJECTED → override type isn't a derivative of the originalOverrideREJECTED →override typeisn't a…create FAILS →class missinguvm_*_utilsregistrationWrong typeeverywhere,or only atone path?Everywhere → built with new, or override set too late?Everywhere →built withnew, oroverride set…BYPASS →constructed withnew instead oftype_id::createTOO LATE → override registered after the create resolvedTOO LATE →overrideregistered afterthe create…One path only →instance-overridepath stringmismatch
Figure 1 — the factory-bug decision tree. Start from the symptom. If the failure is LOUD (an error), it's either a rejected override (the override type doesn't extend the original) or a failed create (the class was never registered). If it's SILENT (no error, but the wrong type is built), ask where: everywhere → it's a bypass (new instead of create) or a too-late override (registered after the create resolved); only at one path → it's an instance-path mismatch. Five leaves, five suspects — the symptom tells you which branch to take.

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.

the five common factory bugs — each as a code shape
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

Bypass vs Too-Late: same symptom, different cause and fixinvestigatesite used newsite used createoverride setafter createoverride setbefore createSymptom: got base type,no erroroverride silently ignoredConstruction site: new orcreate?the first distinguishingquestionBYPASSfix the site → usetype_id::createOverride timing: beforeor after?relative to the createTOO LATEfix ordering → set overridebefore createCorrectcreate +override-before-build12
Figure 2 — separating the two silent 'got the base type' bugs. Both produce identical behaviour: the override is ignored, you get the base type, no error. But the cause differs. Bypass: construction used new, so the create request never reached the factory — fix at the construction site. Too-Late: construction used create correctly, but the override was registered after that create resolved — fix the ordering. The test that separates them: look at the construction site (new vs create) and the override timing (before vs after the create).

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.

During top-down build_phase, an override registered after the child create resolves too late to applytop-down build: override must precede the create it targetstop-down build: override must precede the create it targets1Test build_phase begins (top)the test runs first — this is the only safe window to register theoverride.2Register override here → appliesset_type_override BEFORE super.build_phase(): in place before anychild is created.3super.build_phase() builds childrenenv and its driver are created NOW via the factory — overrides inplace are applied.4Register override here → too lateafter super.build_phase(), the driver already resolved to the basetype — the override misses.
Figure 3 — why override-after-super.build_phase() is too late. build_phase runs top-down: the test builds first, then its children. If the test registers the override BEFORE super.build_phase(), the override is in place when the env (and its driver) are created — it applies. If the test calls super.build_phase() FIRST, the env and driver are created immediately (resolving to the base type), and the override registered afterward arrives after those creates already resolved — it misses. The override must be registered before the create it targets runs.

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 cycles
Too-Late: the override is registered after the create has already resolvedGOOD: ovr_set first — override is in the factory before the createGOOD: ovr_set first — …GOOD: create resolves → ERR (override applied)GOOD: create resolves …BAD: create resolves first → BASE (no override yet)BAD: create resolves f…BAD: ovr_set arrives too late — resolved_type stays BASEBAD: ovr_set arrives t…clkcaseGOODGOODGOODGOODGOODBADBADBADBADBADovr_setcreate_resolveresolved_type----ERRERRERR--BASEBASEBASEBASEt0t1t2t3t4t5t6t7t8t9
Figure 4 — both events happen at zero time during build_phase, but order decides the outcome. ovr_set pulses when the override is registered; create_resolve pulses when the driver's create runs. In the GOOD ordering, ovr_set precedes create_resolve, so resolved_type shows ERR (overridden). In the BAD ordering (override after super.build_phase), create_resolve happens first and resolves to BASE, then ovr_set arrives too late — resolved_type stays BASE. Same two events, opposite order, opposite result.

The 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

Symptom

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.

Root cause

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:

why a correct override applied to nothing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_driver

This 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.)

Diagnosis

The tell is a silent, valid override that applies to nothing — and print_topology confirming the base type was built. Diagnose it:

  1. Confirm it's silent, not loud. No factory error means the override wasn't rejected (so it's a derivative — not Wrong-Family) and create didn't fail (so it's registered — not Unregistered). Silent narrows you to Bypass, Too-Late, or Wrong-Address.
  2. Check the construction site. Confirm the component was created with type_id::create, not new. If new, it's Bypass, not Too-Late. (Here it was create — so not Bypass.)
  3. 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.
  4. Confirm with topology. print_topology showing 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.
Prevention

Register overrides before the construction they target:

  1. Set type overrides before super.build_phase(). In a test, register all factory overrides at the top of build_phase, before calling super.build_phase() — so they're in the factory when the env and its children are created.
  2. 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.
  3. Verify with print_topology. After adding an override, confirm via print_topology (or +UVM_TESTNAME run 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 new instead of type_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 targeted create already resolved to the base type; the override comes too late. Register overrides before super.build_phase().
  • Making the override type not a derivative of the original. The factory rejects an override whose type doesn't extend the original (the original-typed handle can't hold it). The override must be a subclass.
  • Forgetting the uvm_*_utils registration. No proxy, so create fails 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_topology that 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

  1. 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.
  2. 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.
  3. Fix the ordering. A test calls super.build_phase() then set_type_override*. Rewrite it so the override applies, and explain in one sentence why the original missed.
  4. 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 (new instead of create — silent), Too-Late (override registered after the create resolved — silent), Wrong-Family (override not a derivative — loud, rejected), Unregistered (missing uvm_*_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 (new vs create), then the override timing (before vs after the create).
  • Too-Late is a timing bug, not a correctness bug: because build_phase is top-down and each create resolves against the overrides present at that instant, an override registered after super.build_phase() misses — the fix is to register it before.
  • The durable rule of thumb: construct through the factory (type_id::create, never new), register overrides before the construction they target (before super.build_phase()), make the override type a derivative of the original, register every class with uvm_*_utils, and verify with print_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.