The interviewer draws two boxes. "The test sets a type override from axis_driver to drv_a, then an instance override at *.in_agt.drv to drv_b, then another instance override at uvm_test_top.env.in_agt.drv to drv_c. What gets built at uvm_test_top.env.in_agt.drv?" Most candidates say drv_c, because it is the most specific path. It is drv_b. This lesson is the one place in the course where the lookup rules are taught; every other lesson links here.
Think of a hotel front desk with two binders. The first binder holds instance notes ("the guest in room 3 gets the suite"), in the order they were written. The clerk reads from the top and acts on the first note that matches the room; later notes are never read. Only if no instance note matches does the clerk open the second binder, the type notes ("anyone booked as standard gets deluxe"), where each booking type has at most one live entry. Whatever the binders say, the clerk then checks the binders again for the new room type, in case it has its own note. That second look is chaining.
Path specificity is not part of the checklist. A broad instance pattern written first beats a narrow one written later. This surprises people because config_db also ignores specificity, for a different reason; its rules live in config_db resolution order.
For a component, the path is the parent's full name, a dot, and the child's name: the agent's create("drv", this) inside uvm_test_top.env.in_agt is resolved for uvm_test_top.env.in_agt.drv. For an object, the path is the context string plus the name (or just the name with no context). Instance override patterns are glob-matched against this string, so * and ? work.
set_inst_override_by_type(original, override, full_inst_path) appends to an ordered list. The lookup walks the list from the start and stops at the first entry whose original type and path both match. There is no replace flag on instance overrides. So register the most specific instance overrides first, then the broader ones.
// snippet: in a test's build_phase, before the env creates its children
// The narrow override first, so it is found first.
axis_driver::type_id::set_inst_override(drv_c::get_type(), "env.in_agt.drv", this);
axis_driver::type_id::set_inst_override(drv_b::get_type(), "env.*.drv", this);
// type_id::set_inst_override(type, path, this) prefixes the path with this
// component's full name: "uvm_test_top.env.in_agt.drv".set_type_override_by_type(original, override, replace = 1) keeps one live entry per original type. A second type override for the same type replaces the first when replace is 1 (the default); with replace = 0 the existing entry is kept and the new one is ignored. A type override applies to every path, but any matching instance override beats it, whenever each was registered.
After choosing a winner, the factory looks up overrides for the winner's type at the same path. So axis_driver → axis_truncating_driver plus axis_truncating_driver → axis_trunc_logging_driver builds the logging one wherever an axis_driver is requested. A cycle (A → B, B → A) is detected and reported as OVRDLOOP. Chains are useful when one test adds a refinement on top of a base test's override, without knowing about it.
The factory is a single object for the whole simulation. An override applies to every matching create from the moment it is registered, in every part of the bench, including objects created during run_phase. A type override on axis_item therefore changes the items the monitors create too, not only the stimulus. There is no call to remove an override, so a test that needs a substitution only in one place uses an instance override with a path, not a type override and a hope.
Order set | Override | Matches uvm_test_top.env.in_agt.drv? | Outcome |
|---|---|---|---|
1 | type: axis_driver → drv_a | yes (every path) | checked last |
2 | instance | yes | first instance match: drv_b wins |
3 | instance | yes | never reached |
Swap the order of the two instance overrides and drv_c wins. Remove both and drv_a wins. Nothing about how narrow a path is changes the answer.
The factory explains its own decision. debug_create_by_type runs the same lookup without building anything and prints every override it considered, with the one it chose marked. In a test you would call:
// snippet: in the test's end_of_elaboration_phase
uvm_factory::get().debug_create_by_type(axis_driver::get_type(), "uvm_test_top.env.in_agt", "drv");
uvm_factory::get().print(); // registered overrides (and user types)Run on a scratch copy of the bench with drv_a, drv_b and drv_c added and the three overrides registered in the table's order, print() shows the registered overrides in order, and the topology shows what was built:
#### Factory Configuration (*)
Instance Overrides:
Requested Type Override Path Override Type
-------------- --------------------------- -------------
axis_driver *.in_agt.drv drv_b
axis_driver uvm_test_top.env.in_agt.drv drv_c
Type Overrides:
Requested Type Override Type
-------------- ---------------------------
axis_driver drv_a
UVM_INFO dbg_tb.sv(2000) @ 0: uvm_test_top [OVR] in_agt.drv is drv_bThe instance overrides are listed in registration order, which is the order the lookup walks: drv_b is first and wins, exactly as the table predicts.
The type override is listed separately; it would apply only to a path no instance override matches.
The bench's own evidence is simpler: dma_truncate_test registers one type override, there are no instance overrides, so Rule 3 applies at uvm_test_top.env.in_agt.drv and the log line comes from the derived class:
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3A team had a base test that registered set_inst_override_by_type(axis_driver::get_type(), slow_driver::get_type(), "*") to slow every stream driver. A derived test added an instance override for uvm_test_top.env.in_agt.drv to an error-injecting driver. The error driver never appeared. The derived test's build_phase called super.build_phase first, so the base test's broad "*" entry was already first in the list, and the first match won.
Two fixes, both legitimate. Register the derived test's narrow override before calling super.build_phase, so it is earlier in the list; or make the error driver extend slow_driver and add a type override slow_driver → err_driver, so chaining refines the base test's choice. The second is more robust, because it does not depend on call order across a test hierarchy.
set_type_override_by_type(axis_driver, drv_a) and later set_type_override_by_type(axis_driver, drv_b, 0). What gets built for in_agt.drv?drv_a. The second call has replace = 0, so the existing entry for axis_driver is kept and the new one is ignored (UVM reports it at medium verbosity). With the default replace = 1, drv_b would replace it.
axis_driver → drv_x, then type drv_x → drv_y, then instance *.in_agt.drv: axis_driver → drv_z. What is built at uvm_test_top.env.in_agt.drv, and at a hypothetical uvm_test_top.env.aux_agt.drv?At in_agt.drv: the instance override matches first, so the winner is drv_z; it has no override of its own, so drv_z is built. At aux_agt.drv: no instance match, the type override gives drv_x, and resolving again finds drv_x → drv_y, so drv_y is built through the chain.
Order depends on where each test calls super.build_phase relative to its own overrides, which a derived-test author may not control. Chaining is order-independent: derive the new class from the base test's override class and type-override that class, so the base choice is refined rather than raced.
Next: the rules assume the override is registered before the create runs, and that the new class's code actually executes. Override timing, safety and debug covers both, with a real bench bug.