Skip to content

UVM

Override Best Practices

The disciplines that keep factory overrides predictable at scale — declare them in one place early, choose the narrowest scope, register by type not string, keep chains shallow, override only at intended seams, and always verify.

UVM Factory · Module 6 · Page 6.7

The Engineering Problem

The previous chapter catalogued the recurring factory bugs (Module 6.6). Most of them aren't accidents — they're symptoms of missing discipline. The factory's power is substitution-without-modification (Module 6.1), and that power scales: a mature environment has many tests, each registering overrides to shape the same reusable env into a different scenario. Without conventions, that flexibility curdles into unpredictability — an override in one test surprises another, a chain of overrides nobody can follow, a substitution that silently didn't apply, a type override that changed far more than intended.

The problem this chapter solves is keeping a factory-driven environment predictable as it grows. The bugs of 6.6 are prevented not by debugging skill but by a handful of habits applied every time you register an override: declare overrides in one place, early (the top of the test's build_phase, before super.build_phase()); choose the narrowest scope that does the job (instance override for one path, type override for everywhere); register by type, not by string (type-safe, refactor-safe); keep override chains shallow so the resolved type is obvious; override only at intended seams, not everywhere; and always verify the override took effect, because most factory bugs are silent. These six disciplines turn the factory from a source of surprises into a predictable control surface. This chapter is those disciplines and why each one matters.

What disciplines keep factory overrides predictable at scale — where to declare them, how to scope them, how to register them safely, how to keep chains readable, where to apply them, and how to verify them — and why each prevents a class of factory bug?

Motivation — why each discipline exists

Each discipline maps directly to a failure mode it prevents, and to the structural reason the factory needs it:

  • One place, early — because overrides are top-down policy with a timing constraint. Substitution is declared by the test (the top of the hierarchy) and must be registered before the construction it targets (Module 6.6's too-late bug). Declaring all overrides together at the top of build_phase, before super.build_phase(), satisfies the timing rule and makes the test's substitution policy auditable in one glance.
  • Narrowest scope — because a type override is global. A type override swaps a class everywhere it's created; an instance override swaps it at one path. Using a type override when you meant one instance is the classic over-broad surprise — you fixed one agent and silently changed three others. Choose the scope that matches the intent exactly.
  • By type, not string — because strings are fragile. set_*_override_by_type(..., get_type()) is checked by the compiler and survives renames; set_*_override_by_name("my_driver", ...) is a string that a typo or refactor silently breaks (a cousin of the instance-path-mismatch bug). Prefer the type-safe form.
  • Shallow chains — because deep chains hide the resolved type. Override A→B, then B→C, then C→D, and the type actually built is four hops from what you read. Keeping chains shallow (ideally one hop) keeps the resolved type obvious and debuggable.
  • Intended seams only — because an unbounded override surface is unauditable. If everything is overridable and overridden, no one can reason about what the env actually is. Override at deliberate extension points (driver, sequence, monitor, scoreboard policy), so the override surface is small and intentional.
  • Always verify — because most factory bugs are silent. Three of the five common bugs (bypass, too-late, path-mismatch) produce no error. A clean run is not proof. print_topology shows ground truth, so verification is not optional.

The motivation, in one line: each discipline is the direct countermeasure to a way the factory's flexibility turns into unpredictability — timing, scope, fragility, depth, surface, and silence — so practicing all six keeps a growing environment predictable.

Mental Model

Hold the override disciplines as running a control panel at the top of the machine:

The test is the override control panel, and good practice is operating it cleanly. All the switches are in one place (the top of the test's build_phase) so anyone can read the whole substitution policy at a glance — not hunted down across sequences and components. You set every switch before you start the machine (before super.build_phase()), because a switch flipped after the machine is running changes nothing already built. You use the smallest switch that does the job — a single-instance switch for one component, the master type-switch only when you truly mean every instance — so you never change more than you intended. You label switches by what they are, not by a written name on a sticky note (by type, not string), so a relabel elsewhere can't silently mis-wire them. You avoid switches that flip other switches that flip other switches (shallow chains), so the panel's effect is legible. And after setting them, you read the indicator lights (print_topology) to confirm the machine is wired the way the panel says — because the panel can lie silently. Centralized, pre-start, minimal, typed, shallow, verified: a control panel you can trust.

So every time you reach for an override, you're operating that panel: put it with the other switches (one place), set it before build, pick the narrowest switch, label it by type, keep it one hop, and check the lights. Undisciplined overrides are switches scattered around the machine, flipped at random times, each changing more than you think — exactly the 6.6 bugs.

Visual Explanation — the test is the override-policy layer

The first and most structural discipline is where overrides live: in the test, at the top, declaring policy that the whole tree below inherits. The test is the composition root; overrides are its wiring decisions.

The test is the policy layer where overrides are declared; they apply downward to env, agents, and driversOverrides declared at the top (test), applied down the treeOverrides declared at the top (test), applied down the treeTest — the override control panelALL overrides declared here, at the top of build_phase, before super.build_phase() — one auditable place, correct timing.ALL overrides declared here, at the top of build_phase, before super.build_phase() — one auditable place, correct timing.Environmentbuilt by super.build_phase(); its creates resolve against the overrides the test already registered.built by super.build_phase(); its creates resolve against the overrides the test already registered.Agents · sequencers · monitorscreated down the tree — overridden types substituted here automatically.created down the tree — overridden types substituted here automatically.Drivers · the overridden leavesthe err_driver (or other override) actually appears here — verify with print_topology.the err_driver (or other override) actually appears here — verify with print_topology.
Figure 1 — overrides belong in the test, the policy layer at the top. The test declares all overrides at the top of its build_phase (before super.build_phase), and they apply down the whole tree as the env, agents, and drivers are created. Overrides scattered into sequences or components (dashed, discouraged) are hard to find and easy to mis-time. Centralizing them in the test makes the substitution policy auditable in one place and guarantees correct timing.

The structure is a top-down policy flow. The test sits at the top of the component hierarchy and is the natural — and correct — place to declare substitution policy, because substitution is fundamentally a top-down decision (Module 6.1): "for this test, build the env with an error driver." Declaring every override together at the top of the test's build_phase, before super.build_phase(), does three things at once: it satisfies the timing rule (overrides registered before the env is built — Module 6.6), it puts the entire substitution policy for that test in one readable block (so a reviewer sees exactly how this test reshapes the env), and it keeps overrides out of the reusable components below (which must stay scenario-agnostic to be reusable). The anti-pattern — overrides scattered into sequences, configuration objects, or deep components (the dashed discouraged path) — makes the policy impossible to audit and easy to mis-time. So the first discipline is structural: the test is the override control panel, and every override goes on that panel, not loose in the machine.

RTL / Simulation Perspective — the disciplines in code

The six disciplines are concrete coding habits. Seeing them in one test's build_phase shows how they combine into a clean override block.

a disciplined override block in a test's build_phase
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class err_inject_test extends base_test;
  `uvm_component_utils(err_inject_test)
 
  function void build_phase(uvm_phase phase);
    // ── (1) ONE PLACE, EARLY: all overrides here, before super.build_phase() ──
    // ── (3) BY TYPE, not by string: get_type() is compiler-checked, refactor-safe ──
    set_type_override_by_type(            // (2) TYPE scope: every my_driver becomes err_driver
      my_driver::get_type(), err_driver::get_type());
 
    set_inst_override_by_type(            // (2) INSTANCE scope: only THIS monitor, by exact path
      "env.agent0.mon", base_monitor::get_type(), logging_monitor::get_type());
 
    super.build_phase(phase);             // env built AFTER overrides registered → they apply
  endfunction
endclass
// ── (4) SHALLOW: err_driver extends my_driver directly — one hop, obvious resolved type ──
// ── (5) SEAMS: overriding the driver and a monitor — deliberate extension points, not leaves ──
// ── (6) VERIFY (elsewhere): print_topology() / +UVM factory print confirms the types built ──

Read the disciplines off the block. (1) One place, early: both overrides sit together at the top of build_phase, before super.build_phase(), so they're in the factory when the env is built — auditable and correctly timed. (2) Narrowest scope: the driver swap is a type override (genuinely every my_driver), while the monitor swap is an instance override at the exact path env.agent0.mon (only that one) — each scope matches its intent. (3) By type, not string: both use get_type() rather than a name string, so a rename of my_driver is caught by the compiler instead of silently breaking. (4) Shallow: err_driver extends my_driver directly — one hop, so the resolved type is obvious. (5) Intended seams: the overrides target a driver and a monitor — deliberate extension points — not arbitrary leaves. (6) Verify: separately, print_topology confirms the env was actually built with err_driver and logging_monitor. One small block, all six disciplines — and it's exactly this block that a reviewer can read and trust.

Verification Perspective — choosing type vs instance scope

The discipline engineers get wrong most often is scope — reaching for a type override (global) when an instance override (local) is what they mean. The choice is a simple, mechanical question, and getting it right prevents the most common over-broad surprise.

Decision tree for choosing between a type override and an instance overrideeveryspecificif intent was'one'I want tosubstitute atype for thistestEvery instance of the type, or only specific ones?Everyinstance ofthe type, oronly specifi…Every instance → TYPE override (set_type_override_by_type) — global, one lineEvery instance →TYPE override(set_type_override_by_type)— global, one…Only specific paths → INSTANCE override (set_inst_override_by_type) at each exact pathOnly specificpaths → INSTANCEoverride(set_inst_overr…Trap: type override when you meant one instance → silently changes all the othersTrap: typeoverride whenyou meant oneinstance →…
Figure 2 — choosing override scope. Ask: do you want EVERY instance of this type substituted, or just specific ones? Every instance → type override (set_type_override_by_type) — global, concise. Only specific paths → instance override (set_inst_override_by_type) at each exact path — surgical, bounded. The failure is using a type override when you meant one instance: it silently changes every other instance too. Match the scope to the intent exactly.

The scope question is binary and the answer should be deliberate. A type override (set_type_override_by_type) substitutes a class everywhere it is created in the whole environment — it's the right tool when you genuinely want every instance of that type changed (e.g., every driver becomes an error-injecting driver), and it's concise (one line covers the whole tree). An instance override (set_inst_override_by_type) substitutes only at specific hierarchical paths — the right tool when you want to change one or a few components while leaving their siblings alone (e.g., make one agent's monitor a logging monitor). The trap — and the most common over-broad surprise — is using a type override when your intent was one instance: it does change the one you cared about, but it also silently changes every other instance of that type, which may break unrelated parts of the env and is exactly the kind of bug that's hard to trace back. So the discipline is to ask the binary question explicitly every time — every instance, or specific ones? — and pick the scope that matches the intent, defaulting to the narrower (instance) scope whenever you're not sure you mean all.

Runtime / Execution Flow — the override lifecycle as a checklist

The disciplines compose into a small lifecycle that runs every time a test uses an override: declare early, let build apply, verify. Treating it as a fixed sequence makes the disciplines automatic rather than remembered.

The override lifecycle: declare overrides early, build applies them, verify with print_topologydeclare early → build applies → verifydeclare early → build applies → verify1Declare (top of test build_phase)all overrides in one block, narrowest scope, by type — beforesuper.build_phase().2Build applies themsuper.build_phase() constructs the tree; each create resolvesagainst the registered overrides.3Verify (print_topology)confirm the overridden types actually appear in the tree — silentbugs don't announce themselves.4Now you can trust the envthe substitution policy is auditable, correctly timed, and verified— predictable at scale.
Figure 3 — the override lifecycle, as a disciplined sequence. Declare all overrides at the top of the test's build_phase (one place, narrowest scope, by type) → call super.build_phase() so the tree is built with the overrides applied → verify with print_topology that the intended types actually appear. The verify step is non-negotiable because most factory bugs are silent — a clean run is not confirmation. This sequence, run every time, prevents the whole 6.6 bug taxonomy.

The lifecycle makes the disciplines procedural. Step 1 — declare: at the very top of the test's build_phase, register every override in one block, each at the narrowest scope and by type — this single step bundles disciplines 1, 2, 3, and the timing rule. Step 2 — build applies: calling super.build_phase() constructs the tree, and because the overrides are already registered, each create resolves to the intended (possibly overridden) type — the substitution happens automatically, top-down. Step 3 — verify: after build, print_topology (or the simulator's factory print) shows the actual types in the tree, confirming the overrides took effect; this step is non-negotiable precisely because three of the five common bugs are silent (Module 6.6), so a clean simulation is not evidence the override applied. Step 4 — trust: with policy declared in one place, correctly timed, and verified, the environment is predictable — you know what was built, not just hope. Running this three-step lifecycle every single time an override is used is what converts the disciplines from things-you-remember into a reflex that prevents the entire bug taxonomy.

Waveform Perspective — all overrides registered before the first create

The "declare early" discipline is a timing guarantee: every override is registered at the very start of build, before any create runs, so every create in the whole tree sees the complete override policy. The timeline makes that guarantee visible.

Disciplined timing: all overrides registered before any create resolves

10 cycles
Disciplined timing: all overrides registered before any create resolvestest build_phase start: register ALL overrides first (ovr_set)test build_phase start…super.build_phase(): every create resolves against the complete policysuper.build_phase(): e…all resolved_type = ERR — no create missed the overridesall resolved_type = ER…run_phase: the tree, built exactly as the policy declaredrun_phase: the tree, b…clkphaseBLDBLDBLDBLDBLDBLDRUNRUNRUNRUNovr_setcreate_resolveresolved_type----ERRERRERRERRERRERRERRERRt0t1t2t3t4t5t6t7t8t9
Figure 4 — the disciplined ordering. At the very start of the test's build_phase (zero time), all overrides are registered together (ovr_set pulses) before super.build_phase() triggers any construction. Then every create across the tree (create_resolve pulses) resolves against the complete, already-registered override policy — so resolved_type is the intended overridden type (ERR) for all of them. Declaring overrides first means no create can ever miss them: the too-late bug is impossible by construction.

The waveform shows why "declare early" is a guarantee, not just a habit. All the ovr_set pulses happen at the very front of build_phase, before any create_resolve pulse — so by the time the first create runs (when super.build_phase() builds the env), the override policy is already complete in the factory. Every subsequent create across the entire tree therefore resolves against the full policy, and resolved_type is the intended ERR for all of them — none can miss an override, because there is no override registered after any create. Contrast this with the too-late bug (Module 6.6), where an override pulse followed a create pulse and that create resolved to the base type. By front-loading all overrides, the disciplined test makes the too-late bug structurally impossible: there is simply no window in which a create could run before the policy is complete. That's the deeper value of the discipline — it doesn't just usually-work, it removes the failure mode by construction.

DebugLab — the type override that changed more than intended

A type override that fixed one agent and silently broke two others

Symptom

An engineer wanted one agent's driver — env.agent0.drv — to be an error-injecting err_driver, to test how the DUT handled a faulty source. They wrote set_type_override_by_type(my_driver::get_type(), err_driver::get_type()) at the top of the test's build_phase, correctly and early. Agent 0 now injected errors as intended — but the scoreboard suddenly reported failures from agent1 and agent2 as well, which were supposed to drive clean traffic. The override was timed perfectly and registered cleanly; it just affected far more than one agent.

Root cause

A type override was used where an instance override was meant: the type override substituted err_driver for my_driver everywhere, including the other two agents:

why one intended override changed three drivers
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
intent:   env.agent0.drv should be err_driver; agent1.drv, agent2.drv stay my_driver
used:     set_type_override_by_type(my_driver, err_driver)   // GLOBAL — every my_driver
result:   env.agent0.drv = err_driver   ✓ (intended)
          env.agent1.drv = err_driver   ✗ (silently changed)
          env.agent2.drv = err_driver   ✗ (silently changed)
fix:      use an INSTANCE override at the exact path:
          set_inst_override_by_type("env.agent0.drv",
            my_driver::get_type(), err_driver::get_type());   // only agent0

This is a scope discipline failure — the over-broad-surprise from Figure 2. A set_type_override_by_type is global: it changes every instance of the type in the whole environment. The engineer's intent was a single instance (env.agent0.drv), but the type override doesn't know that — it dutifully substituted err_driver for every my_driver, so all three agents got error drivers. Agent 0 looked correct (it was meant to change), which is what made the bug confusing: the intended change worked, masking the two unintended ones, and because the factory silently produced the wrong types for agents 1 and 2 (no error), the only visible symptom was downstream scoreboard failures. The fix is to match scope to intent: an set_inst_override_by_type at the exact path env.agent0.drv changes only that driver and leaves the siblings as my_driver.

Diagnosis

The tell is "my override worked where I wanted and somewhere I didn't." Diagnose scope problems:

  1. Confirm the override itself is correct. Right types, derivative, registered early, by type — if all clean (as here), the bug isn't timing or registration; suspect scope.
  2. Check type vs instance. A set_type_override* is global — every instance of the type. If you intended only specific paths, this is the over-broad bug.
  3. Use print_topology to see the blast radius. It shows every component's actual type — so you immediately see that agent1 and agent2 also became err_driver, confirming the override was too broad.
  4. Trace the unexpected failures to substituted types. Scoreboard failures from components you didn't mean to change point back to a type override that caught them.
Prevention

Match override scope to intent every time:

  1. Default to the narrowest scope. When you mean specific components, use set_inst_override_by_type at exact paths; reserve set_type_override_by_type for when you truly mean every instance of the type.
  2. Ask the binary question explicitly. Before registering, ask "every instance, or specific ones?" (Figure 2) and pick the scope that matches — don't reach for type override by reflex.
  3. Verify the blast radius with print_topology. After registering, confirm only the intended components changed and the siblings are untouched — the verify discipline catches over-broad overrides immediately.

The one-sentence lesson: a type override is global — it substitutes every instance of the type — so when you mean to change one component, use an instance override at its exact path; matching override scope to intent (and verifying the blast radius with print_topology) prevents the most common over-broad factory surprise.

Common Mistakes

  • Scattering overrides across sequences and components. Overrides belong in the test, at the top of build_phase, in one auditable block — not buried where they're hard to find and easy to mis-time.
  • Using a type override when you meant one instance. A type override is global and silently changes every instance; use an instance override at the exact path for surgical changes.
  • Registering overrides by name string. set_*_override_by_name("my_driver", ...) breaks silently on a typo or rename; use the type-safe *_by_type(..., get_type()) form.
  • Registering overrides after super.build_phase(). The targeted creates already resolved; declare overrides before super.build_phase() so they're in place when the tree is built.
  • Building deep override chains. A→B→C→D makes the resolved type four hops from what you read; keep chains shallow (ideally one hop) so the built type is obvious.
  • Overriding everywhere instead of at intended seams. An unbounded override surface is unauditable; override at deliberate extension points so the substitution surface stays small and intentional.
  • Assuming a clean run means the override applied. Most factory bugs are silent; always verify with print_topology that the intended types are actually in the tree.

Senior Design Review Notes

Interview Insights

Six disciplines keep factory overrides predictable. First, declare all overrides in one place, early — at the top of the test's build_phase, before super.build_phase() — so the substitution policy is auditable in one block and is registered before the env is built (correct timing). Second, choose the narrowest scope: an instance override at an exact path when you mean specific components, a type override only when you genuinely mean every instance of the type. Third, register by type using get_type() rather than by name string, so the override is compiler-checked and survives renames. Fourth, keep override chains shallow — ideally one hop — so the resolved type is obvious rather than several substitutions removed from what you read. Fifth, override only at intended seams — deliberate extension points like the driver, sequence, monitor, or a scoreboard policy — not at arbitrary leaves, so the override surface stays small and auditable. Sixth, always verify with print_topology that the intended types actually appear in the tree, because most factory bugs are silent and a clean run is not proof. The unifying idea is that the test is the override control panel: centralize the switches, set them before the machine starts, use the smallest switch for the job, label them by type, keep them shallow, and check the indicator lights. Each discipline maps directly to a bug it prevents — too-late, over-broad, string-fragility, hidden chains, unauditable surface, and silent failure.

Exercises

  1. Audit the panel. Given a test that registers overrides — some in build_phase before super.build_phase(), some in a sequence, one by name string — list which disciplines are violated and how to fix each.
  2. Pick the scope. For (a) "every driver injects errors" and (b) "only env.agent2.mon logs," choose type vs instance override and write the registration line for each.
  3. Make too-late impossible. Explain, using the timing of build_phase, why declaring all overrides before super.build_phase() makes the too-late bug structurally impossible — not just unlikely.
  4. Verify the blast radius. Describe how you'd use print_topology to confirm a type override changed only the components you intended, and what an over-broad override would look like in its output.

Summary

  • Predictable factory use comes from six disciplines, each preventing a class of 6.6 bug: one place, early (timing + auditability), narrowest scope (no over-broad surprises), by type not string (refactor-safe), shallow chains (obvious resolved type), intended seams only (auditable surface), and always verify (silent bugs).
  • The test is the override control panel: all overrides declared together at the top of the test's build_phase, before super.build_phase() — the structural rule that makes substitution policy auditable and correctly timed.
  • Scope is the most common failure: a type override is global (every instance), an instance override is surgical (exact paths); ask "every instance, or specific ones?" and default to the narrower scope when unsure.
  • Declaring overrides before any create makes the too-late bug structurally impossible — there's no window in which a create could run before the policy is complete — and verification with print_topology is non-negotiable because most factory bugs are silent.
  • The durable rule of thumb: treat the test as a control panel — centralize every override in one early block, scope it as narrowly as the intent allows, register it by type, keep chains shallow, override only at deliberate seams, and verify the built tree with print_topology — because disciplined overrides are the difference between a factory that scales predictably and one that surprises you.

Next — Configuration Database Architecture: the factory substitutes types from the top; the configuration database passes values and handles the same way — top-down, without modifying the components in between. The next module opens with how the config DB is structured and why it's the second half of UVM's top-down control story.