UVM
Raise Objection
How raise_objection works in detail — the call signature with its optional description and count, where to raise (test, sequence, or component), how an objection propagates up the hierarchy, and why the raise must come before any simulation time advances.
Objections · Module 17 · Page 17.2
The Engineering Problem
Objections hold a phase open while work is in flight, and the phase ends when the count returns to zero (Module 17.1). This chapter is the raise — the act of registering that you have work, incrementing the count, holding the phase open. It looks trivial — phase.raise_objection(this) — but it carries real decisions: where to raise (the test? the sequence? a component?), what the optional description and count arguments do, how the objection propagates up the component hierarchy to the phase, and — critically — when to raise relative to the work. That last point is subtle and sharp: the raise must come before the work and before any simulation time advances — because the phase checks the count as soon as it can, and if any time passes with the count at zero (a delay before the raise, a raise inside a forked process), the phase ends in that window, cutting off the work even though a raise was written. The problem this chapter solves is raising correctly: the call, its arguments, where it belongs, how it propagates, and the timing that makes it actually hold the phase open.
raise_objection is the call a component makes to register pending work and increment the phase's objection count. Its signature is phase.raise_objection(this, description, count) — this is the objecting component; description is an optional string (for tracing/debug — why you're objecting); count is an optional amount (default 1 — raise by N at once). The raise increments the count by count and propagates up the component hierarchy — an objection raised on a component bubbles up through its parents to the phase, so the phase's total count reflects all raises everywhere. You raise in the component that owns the work: the test (before stimulus), a sequence (bracketing its own body — best via the automatic phase objection), or any component with phase-holding work. And the raise must come before any time-consuming step — synchronously, at the very start — so the count is positive from time zero, before the phase can end. This chapter is the raise in detail: the signature and arguments, where to raise, hierarchical propagation, and the before-any-time-advances timing.
How does
raise_objectionwork — itsdescriptionandcountarguments, where to call it (test, sequence, or component), how it propagates up the hierarchy, and why must the raise come before any simulation time advances?
Motivation — why the raise's details matter
The raise seems like a one-liner, but each of its aspects exists for a reason. The motivations:
- The
thisidentifies the objector — for propagation and debug. The objecting component (this) is recorded so the objection propagates up its hierarchy and so tracing can show who objected. It's not optional — the objection belongs to a component. - The
descriptionmakes objections debuggable. When a phase won't end (a stuck objection, Module 17.6), you need to know which objection and why. A meaningfuldescription("running soak sequence") is what tracing shows — without it, debugging objections is guessing. - The
countlets you raise by more than one. Occasionally a component starts N parallel units of work and raises bycount = Nat once (balanced by N drops). Usually it's1, but the argument exists for batched work. - Where you raise determines what holds the phase. The test raising holds the phase for the whole test; a sequence raising holds it for that sequence. Choosing where (and using the sequence auto-objection) is how you make the phase track the right work.
- The timing of the raise is decisive. The phase ends the instant the count is zero. So the raise must make the count positive before the phase can check it — before any time advance. A late raise (after a delay, inside a fork) leaves a window where the phase ends, defeating the raise.
The motivation, in one line: the raise's details — the objector (this, for propagation and debug), the description (for debuggability), the count (for batched work), the placement (which component holds the phase), and the timing (before any time advances) — each governs whether and how the objection correctly holds the phase open, so raising well is more than writing raise_objection.
Mental Model
Hold raising an objection as clocking in at the start of a job — registering your name and what you're doing, before you begin:
Raising an objection is clocking in: before you start a job, you sign the register with your name and a note about what you're doing, and the workplace stays open while anyone is clocked in. You must clock in before you start — sign in after you've begun, and the place might already have closed in the gap. Picture a workplace (the phase) that stays open while anyone is clocked in and closes when the register is empty. To start a job, you clock in (raise an objection): you sign the register with your name (
this— who's working) and, ideally, a note (description— what you're doing, "running the soak sequence") so that later, if the workplace won't close, the manager can read the register and see who's still clocked in and why (objection debug). If you're starting several jobs at once, you can clock in for all of them (count = N). The register tallies everyone clocked in (the count), and it propagates: if you're part of a team, your clock-in counts toward the building's total up the chain (hierarchical propagation — a component's objection bubbles up to the phase). The crucial discipline is the timing: you must clock in before you start working — the moment you arrive, before doing anything that takes time. If you start the job first and clock in later (sign the register after a delay, or inside a process that runs later), there's a gap where the register was empty — and the workplace, seeing an empty register, closes in that gap, locking you out mid-job. So you clock in synchronously, at the very start: sign first, then work — so the register is never empty while you have work to do.
So raising an objection is clocking in: register your name (this) and what you're doing (description), for one or more jobs (count), before you start — and the tally (count) propagates up to keep the phase open while you're clocked in. The discipline is clock in first, before any time passes — sign the register synchronously at the very start, or a gap with an empty register lets the phase close and cut off your work. Sign in before you start; the workplace stays open only while the register shows someone working.
Visual Explanation — the raise call and its effect
The defining picture is the call: raise_objection(this, description, count) increments the count and propagates up the hierarchy to the phase.
The figure shows the raise call and what it does. A component (which owns the work) calls phase.raise_objection(this, description, count). The arguments each play a role: this identifies the objector (for propagation and debug), description is an optional string that aids debugging (it's what tracing shows), and count (default 1) is the amount to raise by. The raise increments the objection count (+= count) and propagates up the component hierarchy to the phase — so the phase's total count includes this raise, and the phase (now seeing a positive count) is held open. The crucial reading is the two effects of a single raise: it registers who is objecting (and why, via description) and it bumps the count that keeps the phase alive. The warning-colored count is incremented and propagates — it's not a local tally on the component but a contribution to the phase's global count (an objection anywhere in the hierarchy bubbles up to the phase). The success-colored phase, seeing the count go positive, is held open — the purpose of the raise. The diagram is the anatomy of the raise: a component calls raise_objection with its identity, a reason, and an amount, incrementing the phase's count and recording the objection for tracing — holding the phase open while registering who and why. The call is small, but it does two jobs: gate the phase (the count) and enable debugging (the identity and description).
RTL / Simulation Perspective — where to raise, and the auto-objection
In code, you raise in the component that owns the work — the test, or cleanly, the sequence via the automatic phase objection. The code shows both, and the timing rule.
// === IN THE TEST: raise before stimulus, with a meaningful description ===
class my_test extends uvm_test;
task run_phase(uvm_phase phase);
phase.raise_objection(this, "running main stimulus"); // this · description (for debug)
seq.start(env.agt.seqr); // the work
phase.drop_objection(this, "main stimulus complete");
endtask
endclass
// === IN THE SEQUENCE (modern, clean): the sequence auto-raises around its own body ===
class my_seq extends uvm_sequence #(bus_txn);
function new(string name="my_seq");
super.new(name);
set_automatic_phase_objection(1); // ← auto raise/drop around body() for the starting phase
endfunction
task body(); repeat (100) `uvm_do(req) endtask // no manual raise/drop needed — it's automatic
endclass
// === TIMING: raise BEFORE any time-consuming step or fork — never after time advances ===
task run_phase(uvm_phase phase);
phase.raise_objection(this); // ✓ FIRST — count positive from time 0
#10ns; do_work(); // time advances AFTER the raise → safe
// ✗ NEVER: #10ns; phase.raise_objection(this); ← phase may END during the #10ns (count was 0)
phase.drop_objection(this);
endtaskThe code shows where and when to raise. In the test: phase.raise_objection(this, "running main stimulus") — note the meaningful description (it's what tracing shows when debugging a stuck objection, Module 17.6) — before the stimulus, dropped after. In the sequence (the modern, clean pattern): set_automatic_phase_objection(1) in the constructor makes the sequence automatically raise an objection around its body() for its starting phase — no manual raise/drop needed; the sequence that defines the work brackets its own execution. Timing: the raise must come before any time-consuming step or fork — phase.raise_objection(this) first (count positive from time 0), then #10ns; do_work() (time advances after the raise — safe); the ✗ comment marks the trap: #10ns; phase.raise_objection(this) raises after a delay, during which the count was zero and the phase may have ended. The shape to carry: raise in the component that owns the work — the test (with a good description) or, better, the sequence via set_automatic_phase_objection(1) (which auto-brackets the body) — and always raise before any simulation time advances, so the count is positive from time zero. The description is not decoration — it's what makes a stuck objection debuggable; the auto-objection is the cleanest pattern (the work-defining sequence holds its own phase); and the timing (raise first) is what makes the raise actually work.
Verification Perspective — hierarchical propagation
A raise on any component propagates up the hierarchy to the phase — the objection is hierarchical. Understanding the propagation is understanding how a deep component's raise reaches the phase.
The figure shows hierarchical propagation — how a raise reaches the phase. The component that raised calls raise_objection(this); the objection starts there and propagates upward. It bubbles up through the environment and parent components, up to the test, and into the phase's total objection count. The crucial reading is that an objection raised deep in the hierarchy still holds the phase open — it bubbles up to the phase's aggregate count. The success-colored phase sees the sum of all objections raised anywhere, and is held open while that total is positive. The verification insight is that any component can hold the phase open — because every objection propagates up to the same phase-level count. This has practical consequences. First, the total count is the sum across the whole hierarchy — a driver deep in an agent raising an objection contributes to the same count as the test raising at the top. Second, it means who raises is a choice (any level can), but the effect is the same (the phase is held open) — so you raise at the level that owns the work (the test for whole-test work, a sequence for sequence-scoped work), knowing it propagates to the phase regardless. Third, it explains the debugging concern (Module 17.6): when a phase won't end, the stuck objection could be anywhere in the hierarchy — so the description (and the hierarchical objection trace) is what lets you find it. The figure is the propagation model: an objection anywhere bubbles up to the phase's total, so any component's raise holds the phase open, the count is the hierarchy-wide sum, and finding a stuck one means tracing the hierarchy. Raise anywhere, and it reaches the phase — which is why the description matters for finding it later.
Runtime / Execution Flow — the moment of the raise
At run time, the raise is the moment the objection count goes positive — and it must happen before the phase checks the count. The flow shows the raise in time.
The flow shows the raise as a moment in time — and why it must precede any time advance. Begin (step 1): the phase starts with the count at zero — nothing is objecting yet. Raise (step 2): the component raises an objection synchronously, before any time advances — no delay, no fork first — so the count goes positive at once. Propagate (step 3): the objection bubbles up; the phase's total count is now positive. Hold (step 4): with a positive count, the phase stays alive while the work runs. The runtime insight is the timing: the raise must happen before the phase can end, and the phase can end as soon as it checks a zero count — which it can do the moment time tries to advance. So if the raise is synchronous (at the very start, before any #delay or fork), the count is positive before the phase ever checks it — no zero-count window. But if the raise is deferred — after a #delay, or inside a fork ... join_none that runs later — then, between the phase start and the deferred raise, time advances with the count at zero, and the phase ends in that window (the DebugLab). This is why the rule is raise before any simulation time advances: the raise must win the race against the phase's end-check, and the only way to guarantee it is to raise synchronously, first, before yielding to the simulator. The flow is the critical moment: the phase opens with the count zero, and the raise must make it positive before the simulator gets a chance to advance time and find it zero — raise first, synchronously, and the window never opens.
Waveform Perspective — the count goes positive at the raise
The raise is visible on a timeline as the instant the count goes positive and the phase is held open. The waveform contrasts a synchronous raise (safe) with a deferred one (the gap).
A synchronous raise holds the phase from time zero; a deferred raise leaves a gap
12 cyclesThe waveform shows raise timing — synchronous (safe) versus deferred (the gap). With a synchronous raise, raise pulses at the very start (time 0), obj_count goes positive immediately, and phase_active stays high through the work — there is no zero-count window. The count stays positive from time 0 until the drop (cycle 7), when it returns to zero and the phase ends — correctly, after the work. The dashed contrast (late_raise) shows the hazard: if the raise were deferred until cycle 3 (after time advanced), the count would be zero through cycles 1–2 — a gap in which the phase could end before the raise ever took effect. The crucial reading is the difference at the start: the synchronous raise makes obj_count positive at time 0, before the phase can check it; the deferred raise would leave a window (cycles 1–2) where the count is zero and the phase is vulnerable to ending. The picture to carry is that the raise's value is in its timing: a raise at time 0 guarantees the count is positive before any time advances, so the phase is held from the very start; a raise after time advances leaves a gap where the phase can slip through and end. Reading this waveform — is the count positive from time zero, with no gap before the work? — confirms the raise is placed correctly. The count positive from the start, no gap is the signature of a correct raise: it wins the race against the phase's end-check by being synchronous and first, so the phase is held open from the instant the work begins — not a few cycles in, when the window might already have closed.
DebugLab — the raise that fired too late
A phase ending prematurely because the objection was raised after a delay, not before it
A test had a raise_objection — it was right there in the run_phase — yet the test still ended prematurely: intermittently, the stimulus was cut off (sometimes no stimulus, sometimes partial), and the failure varied run to run. The raise was present and looked correct, but the phase ended anyway, before the work finished (or started). The objection was being raised — just, somehow, too late.
The raise was placed after a step that advanced simulation time — so between the phase's start and the deferred raise, the count was zero, and the phase ended in that window, before the raise took effect:
✗ raise AFTER time advances (a delay, or inside a forked process) → zero-count window:
task run_phase(uvm_phase phase);
@(posedge vif.clk); // ✗ time advances HERE — count is still 0
phase.raise_objection(this); // the raise happens too late: phase may have already ended
seq.start(...); phase.drop_objection(this);
endtask
// OR: fork phase.raise_objection(this); ... join_none; ← raise runs in a later process → gap
✓ raise SYNCHRONOUSLY, before ANY time-consuming step → count positive from time 0:
task run_phase(uvm_phase phase);
phase.raise_objection(this); // ✓ FIRST — count positive before any time advances
@(posedge vif.clk); // time advances AFTER the raise → safe
seq.start(...); phase.drop_objection(this);
endtaskThis is the raise-too-late bug — a raise that is written but placed after simulation time advances, leaving a zero-count window in which the phase ends. The phase ends the instant the objection count is zero, and it can check the count as soon as time tries to advance. At the start of run_phase, the count is zero. If the first thing the task does is advance time — a @(posedge clk), a #delay, or forking the raise into a process that runs later — then, during that time advance, the count is still zero, and the phase ends in that window — before the raise ever executes. The raise was present, but it ran too late: the phase had already ended. The intermittence is the tell — whether the phase catches the zero-count window depends on scheduling, so the failure varies run to run (sometimes the work partly runs, sometimes not at all). The fix is to raise synchronously, before any time-consuming step: phase.raise_objection(this) as the very first statement, before any @, #, or fork — so the count is positive from time zero, before the phase can check it. Now there is no zero-count window; the phase is held open from the instant run_phase begins. The general lesson, and the chapter's thesis: the raise must come before any simulation time advances — synchronously, at the very start of the work — because the phase ends as soon as the count is zero and checks it the moment time advances, so a raise after a delay (or inside a forked process) leaves a zero-count window where the phase ends before the raise takes effect; raise first, synchronously, and the window never opens. Clock in the instant you arrive — sign the register before you do anything else, or the place may close in the gap.
The tell is intermittent premature ending despite a raise being present. Diagnose raise-timing errors:
- Check what runs before the raise. Any
@,#, orforkbefore raise_objection advances time with the count at zero. - Look for the raise inside a forked process. A raise in a
fork ... join_noneruns in a later process, after the main thread has moved on — a gap. - Suspect timing when the failure is intermittent. A scheduling-dependent premature end points at a zero-count window, not a missing raise.
- Confirm the raise is the first statement. The raise should precede any time-consuming step in the task.
Raise synchronously, before any time advances:
- Make raise_objection the first statement. Before any
@,#, orfork, so the count is positive from time zero. - Never raise inside a forked process that runs later. Raise in the main thread synchronously; fork the work after the raise if needed.
- Prefer the sequence auto-objection.
set_automatic_phase_objection(1)raises around the body correctly, avoiding manual-timing mistakes. - Verify no zero-count window. Confirm the count is positive before any simulation time elapses in the phase.
The one-sentence lesson: raise the objection synchronously, before any simulation time advances (before any @, #, or fork), so the count is positive from time zero — because the phase ends as soon as the count is zero and checks it the moment time advances, so a raise placed after a delay or inside a forked process leaves a zero-count window where the phase ends before the raise takes effect.
Common Mistakes
- Raising after time advances. A raise after a
@,#, orforkleaves a zero-count window where the phase can end; raise synchronously, first. - Raising inside a forked process. A raise in
fork ... join_noneruns later, after the main thread moved on — a gap; raise in the main thread before forking. - Omitting the description. A raise with no description is hard to trace when a phase won't end; pass a meaningful string for debuggability.
- Raising at the wrong level. Raise in the component that owns the work — the test for whole-test work, the sequence (auto-objection) for sequence work.
- Mismatching count and drops. A raise by
count = NneedsNtotal drops; an imbalance leaves the count nonzero (phase hangs). - Manual raise/drop where auto-objection fits. A sequence's
set_automatic_phase_objection(1)brackets the body correctly; manual raise/drop in a sequence is error-prone.
Senior Design Review Notes
Interview Insights
raise_objection registers that a component has pending work and increments the phase's objection count, holding the phase open. Its signature is phase.raise_objection(this, description, count). The first argument, this, is the objecting component — it identifies who is raising, which matters because the objection propagates up that component's hierarchy and because tracing shows who objected. The second argument, description, is an optional string that aids debugging; it's what objection tracing displays, so when a phase won't end you can see which objection and why. A meaningful description like "running soak sequence" is what makes a stuck objection findable. The third argument, count, is an optional amount to raise by, defaulting to one; occasionally a component starts N parallel units of work and raises by N at once, which must then be balanced by N drops. The effect of the call is twofold: it increments the objection count by the count amount, and it records the objection for tracing. The increment propagates up the component hierarchy to the phase, so the phase's total count includes this raise, and the phase, now seeing a positive count, is held open. So the raise both gates the phase, via the count, and enables debugging, via the identity and description. You call it in the component that owns the work — the test before its stimulus, or, more cleanly, a sequence via the automatic phase objection. And critically, the timing matters: the raise must come before any simulation time advances, so the count is positive from time zero, before the phase can end. The mental model is clocking in: you sign the register with your name and a note about what you're doing, before you start, and the workplace stays open while anyone is clocked in.
Because the phase ends as soon as the objection count is zero, and it can check the count the moment time tries to advance, so the raise must make the count positive before that check happens. At the start of run_phase, the count is zero — nothing has objected yet. If the first thing the task does is advance time — a clock edge, a delay, or forking the raise into a process that runs later — then during that time advance, the count is still zero, and the phase can end in that window, before the raise ever executes. The raise is present in the code, but it runs too late; the phase has already ended. The fix is to raise synchronously, as the very first statement, before any time-consuming step. Then the count goes positive at time zero, before the phase can check it, and there's no zero-count window for the phase to slip through. Concretely, that means raise_objection must come before any at, hash-delay, or fork in the task. A particularly sneaky version is raising inside a fork-join_none: the forked process runs in a later scheduling step, so the main thread moves on and the phase can end before the forked raise executes. The tell for this bug is intermittent premature ending — the test has a raise, but the phase ends early, and the failure varies run to run, because whether the phase catches the zero-count window depends on scheduling. So the rule is to raise first, synchronously, before yielding to the simulator. The raise has to win the race against the phase's end-check, and the only way to guarantee that is to make the count positive before any time advances. The clocking-in analogy captures it: clock in the instant you arrive, before doing anything else, or the place might close in the gap before you've signed the register.
The automatic phase objection is a mechanism where a sequence automatically raises an objection when its body starts and drops it when its body ends, for the sequence's starting phase, so you don't write the raise and drop manually. You enable it by calling set_automatic_phase_objection(1) in the sequence's constructor. Then, when the sequence runs on a sequencer in a phase context, UVM raises an objection for that phase around the sequence's body and drops it when the body completes. The reason to use it is that it puts the objection where it conceptually belongs — with the sequence that defines the work — and it handles the timing and balance correctly, avoiding the common manual-objection mistakes. With manual raise and drop, you have to remember to raise before the work and drop after, get the timing right so the raise precedes any time advance, and ensure every raise is matched by a drop on every path including early returns and exceptions. The automatic phase objection does all of that for you, bracketing the body cleanly. It's the modern, clean pattern for sequence-scoped work: the work-defining sequence holds its own phase open for exactly the duration of its body. This is better than having the test manually bracket every sequence, because it keeps the objection logic with the sequence and scales as you compose sequences. There are still cases where you raise manually — in a component that isn't a sequence, or when the objection scope doesn't match a single sequence body — but for the common case of a sequence that should hold the phase open while it runs, set_automatic_phase_objection(1) is the recommended approach. It reduces boilerplate and eliminates a whole class of objection bugs around timing and balance, which is why it's preferred over manual raise and drop in sequences.
When a component raises an objection, it propagates from that component up through its parents to the phase, and the phase's total objection count reflects the aggregate of all objections raised anywhere in the hierarchy. So an objection raised deep in the hierarchy — say in a driver inside an agent inside an environment — bubbles up through each parent, the agent, the environment, the test, and into the phase's total count. The phase sees the sum of all objections raised anywhere and is held open while that total is positive. This propagation has a few practical consequences. First, any component can hold the phase open, because every objection reaches the same phase-level count; you're not limited to raising only in the test. Second, the total count is a hierarchy-wide sum, so a deep component's raise contributes to the same count as a top-level raise. Third, who raises becomes a choice — any level can — but the effect is the same, the phase is held open, so you raise at the level that owns the work, knowing it propagates regardless: the test for whole-test work, a sequence for sequence-scoped work. Fourth, and importantly for debugging, when a phase won't end because of a stuck objection, that objection could be anywhere in the hierarchy, which is why the description you pass and the hierarchical objection trace matter — they let you find where the stuck objection lives. So propagation means the objection mechanism is hierarchical and aggregate: a raise anywhere bubbles up to the phase's total, any component's raise holds the phase open, the count is the sum across the whole tree, and finding a stuck objection means tracing the hierarchy. This is why the methodology lets objections be raised at whatever level naturally owns the work, while still resolving to a single phase-level decision about when to end.
You raise in the component that owns the work, choosing the level whose scope matches what should hold the phase open. For whole-test work, the test raises in its run_phase before starting stimulus and drops after — this holds the phase for the entire test. For sequence-scoped work, the cleanest approach is to let the sequence hold its own phase open using the automatic phase objection, set_automatic_phase_objection(1), so the sequence raises around its body and drops when done — this keeps the objection with the work-defining sequence and scales as you compose sequences. For work in some other component that must keep the phase alive — a component running a time-consuming process that isn't a sequence — that component raises and drops around its work. The deciding question is which scope should hold the phase open. If the phase should stay alive for the whole test, the test raises. If it should stay alive for the duration of a particular sequence, that sequence raises, ideally automatically. Because objections propagate up to the same phase-level count, raising at any of these levels has the same effect of holding the phase open; the choice is about matching the objection's lifetime to the work's lifetime and keeping the logic where it's maintainable. A common, clean structure is for the top-level sequence to hold the objection for the stimulus phase, often via the automatic phase objection, while monitors and drivers that fork forever don't raise at all, so they don't prevent the phase from ending. You avoid having every sub-sequence raise its own objection, which would be noisy and could hold the phase open longer than intended. So the guidance is: raise at the level that owns the work and whose duration should define the phase, use the sequence automatic phase objection for sequence-scoped stimulus, and don't raise in the perpetual infrastructure components. That keeps the phase's lifetime tied to the meaningful work.
Exercises
- Read the signature. Explain each argument of
raise_objection(this, description, count)and what it's for. - Fix the timing. Given a
run_phasethat does@(posedge clk)then raises, explain the bug and reorder it. - Use the auto-objection. Write a sequence that holds its phase open automatically, and explain what it replaces.
- Trace propagation. Describe how an objection raised in a driver reaches the phase's count.
Summary
raise_objectionregisters pending work and increments the phase's count. Its signature isphase.raise_objection(this, description, count):this(the objecting component),description(an optional string for tracing/debug),count(an optional amount, default1).- The raise increments the count and propagates up the hierarchy — an objection raised on any component bubbles up to the phase's total count, so any component can hold the phase open and the count is the hierarchy-wide sum.
- You raise in the component that owns the work: the test (before stimulus, with a meaningful description), or cleanly the sequence via
set_automatic_phase_objection(1)(which auto-brackets the body) — not in perpetual monitors/drivers. - The raise must come before any simulation time advances — synchronously, at the very start — because the phase ends as soon as the count is zero and checks it the moment time advances, so a raise after a delay or inside a forked process leaves a zero-count window where the phase ends.
- The durable rule of thumb: raise the objection synchronously as the first statement (before any
@,#, orfork), in the component that owns the work (the test, or a sequence viaset_automatic_phase_objection(1)), with a meaningful description for debuggability — the raise increments and propagates to the phase's count, but only holds the phase open if it makes the count positive before any simulation time advances.
Next — Dropping an Objection: the other half of the pair — how drop_objection works, balancing every raise so the count returns to zero, where and when to drop (after the work truly completes, including any drain time), and why a dropped-too-early or never-dropped objection ends the phase wrong.