SystemVerilog · Module 12
Deferred Immediate Assertions
The `assert #0` and `assert final` forms — immediate assertions that defer evaluation to the Observed or Final region so they see settled signals instead of mid-cycle glitches. The simulation-region model, the two forms compared, when to use each, and why combinational checkers belong in deferred form to avoid false-failure noise.
Module 12 · Page 12.3
The Problem Deferred Assertions Solve
Module 12.2 closed with a Debug Lab on the Active-region race condition: an immediate assertion inside a clocked process checking the pre-NBA value of a signal that's being NBA-assigned in the same process. The fix preview pointed to assert #0 — the deferred immediate assertion form — as one of the three valid remediations. This page covers the form in full.
But the Active-region race inside always_ff is only one of the two problems deferred assertions solve. The bigger, more frequent problem is glitches on combinational outputs. An assertion in an always @(*) block that watches out of a combinational network will re-evaluate every time any input changes — including during the brief simulation moments when the input is mid-transition and out is momentarily wrong. Regular immediate assertions fire on those transient values. They report failures the design never actually produced.
module example;
logic a, b, out;
assign out = a & b;
// Fires whenever 'out' changes — including on glitches
always @(out) begin
assert (!out || (a && b))
else $error("Output mismatch at %0t: out=%b a=%b b=%b",
$time, out, a, b);
end
initial begin
a = 0; b = 1;
#5 a = 1; // a transitions 0→1 at T=5; out becomes 1
#2 b = 0; // b transitions 1→0 at T=7; out becomes 0
// Between T=5 and T=7, out=1 (transient — not a bug).
// At T=7, b changes; the assertion re-evaluates on every
// intermediate delta cycle. Some of those evaluations may
// see (out=1, a=1, b=0) — a state that violates the assertion.
// FALSE FAILURE reported. The design is correct.
end
endmoduleBoth problems — the Active-region NBA race and the combinational glitch — have the same root cause: the assertion fires when it sees transient mid-cycle values rather than settled end-of-step values. The deferred forms move the evaluation to a later simulation region where those transients are gone.
The Simulation Region Model
SystemVerilog divides each simulation time step into ordered regions. Understanding which region each assertion form runs in is the whole point of this page — the region determines what values the assertion sees.
| Region | What runs here | Assertion form that fires here |
|---|---|---|
| Preponed | Concurrent-assertion sampling (Module 12.4) | (concurrent only — Module 12.4) |
| Active | always_comb, assign, blocking assignments, #0 delays | Regular immediate assertions |
| Inactive | Re-activated #0 events | — |
| NBA | Non-blocking assignment (<=) updates apply | — |
| Observed | (after NBA settles) | assert #0 evaluates here |
| Reactive | program blocks, final callbacks | — |
| Re-active | Re-activated reactive | — |
| Final | Absolute last point in the time step | assert final evaluates here |
All these regions happen at the same simulation time — e.g. $time is 50ns throughout the entire sequence. The regions are processing-order within that simulation instant, not different times on the wall clock. The implication: a deferred assertion runs at the same $time as a regular immediate assertion, but it sees different values — the values that survived the NBA settle.
T = 50ns:
┌─ Preponed ──┐ (concurrent-assertion sampling — Module 12.4)
│ │
├─ Active ────┤ always_comb runs; `assert (out)` evaluates here
│ always_ff │ ← may see mid-cycle glitches
├─ Inactive ──┤
│ │
├─ NBA ───────┤ `<=` updates apply
│ │
├─ Observed ──┤ ← `assert #0 (out)` evaluates here
│ │ (NBA-settled values; glitches gone)
├─ Reactive ──┤
│ │
└─ Final ─────┘ ← `assert final (out)` evaluates here
(absolute last point — guaranteed settled)Syntax — assert #0 and assert final
The syntax is identical to a regular immediate assertion with the qualifier #0 or final between assert and the parenthesised expression.
// ── Observed deferred (most common) ──────────────────────────────
always @(*) begin
assert #0 (out == (a & b))
else $error("[%0t] Output mismatch — out=%b a=%b b=%b",
$time, out, a, b);
end
// ── Final deferred (absolute last point in the step) ─────────────
always @(*) begin
assert final (out == (a & b))
else $error("[%0t] Final check — out=%b a=%b b=%b",
$time, out, a, b);
end
// ── Same forms work for assume and cover ─────────────────────────
assume #0 (valid_input);
cover #0 (corner_case);
assume final (consistent_state);
cover final (settled_handshake);
// ── Named, with $error message — production form ─────────────────
a_out_stable:
assert #0 (out == (a & b))
else $error("[%0t] a_out_stable: out=%b doesn't match a&b (a=%b b=%b)",
$time, out, a, b);The else fail action, severity tasks ($fatal, $error, $warning, $info), and naming all behave exactly as in Module 12.2. The qualifier #0 / final only affects when in the time-step processing order the evaluation happens.
assert #0 vs assert final — Which to Pick
Both forms defer past the NBA region. The difference is how far past.
| Property | assert #0 (Observed) | assert final (Final) |
|---|---|---|
| Region | Observed (after NBA) | Final (absolute last point) |
Sees program block updates? | No | Yes |
Sees Reactive region updates? | No | Yes |
| Performance overhead | Minimal | Minimal–small |
| Use when | Combinational settle is enough | UVM program-block updates must settle too |
For pure RTL combinational checks (no UVM program blocks, no final callbacks modifying state), assert #0 is sufficient — it waits past the NBA region, where all the racy updates happen. For environments that mix RTL with program-block / UVM testbench code that updates signals in the Reactive region, assert final waits even longer and sees the result of those Reactive updates too.
Default in production: assert #0 for combinational invariants, assert final only when the test environment specifically updates signals in Reactive or program regions (rare in modern UVM-based flows, common in legacy program-block flows).
A Working Example — Combinational Validity Checker
The pattern below shows the canonical use case: a combinational checker on a multiplexer-output relationship, where regular immediate assertions would false-fire on every input transition.
module mux_with_checker (
input bit [1:0] sel,
input bit [31:0] in0, in1, in2, in3,
output bit [31:0] out
);
// Combinational mux
always_comb begin
unique case (sel)
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
2'b11: out = in3;
endcase
end
// ── Regular immediate (BAD for this use case) ─────────────────
// Would fire on every transition while sel and the data inputs
// change at different delta cycles. False failures on every cycle.
//
// always @(*) begin
// assert (out == ((sel == 2'b00) ? in0 :
// (sel == 2'b01) ? in1 :
// (sel == 2'b10) ? in2 : in3))
// else $error("Mux mismatch"); // false positives everywhere
// end
// ── Deferred immediate (correct) ─────────────────────────────
// Waits for the Observed region. By then, sel and all data inputs
// have settled; out has its final value for this time step.
a_mux_output_matches:
always @(*) begin
assert #0 (out == ((sel == 2'b00) ? in0 :
(sel == 2'b01) ? in1 :
(sel == 2'b10) ? in2 : in3))
else $error("[%0t] mux output mismatch: sel=%b out=0x%0h "
+ "in0=0x%0h in1=0x%0h in2=0x%0h in3=0x%0h",
$time, sel, out, in0, in1, in2, in3);
end
endmoduleWhat this code does. Defines a 4-to-1 multiplexer with a combinational checker that verifies out matches the selected input. The commented-out regular immediate form would fire on every input transition — between the moment sel changes and the moment out settles to the new selected value, there is a delta-cycle window where the assertion sees inconsistent state. The deferred form waits for that window to close before evaluating; the assertion sees only the settled state and reports real mismatches without the false-positive noise.
How to simulate it. Compile and instantiate the module in a testbench that drives various (sel, in0..in3) combinations. Compare the assertion logs from a build using the regular form vs the deferred form — the regular form will produce dozens or hundreds of false failures during stimulus transitions; the deferred form will produce zero (assuming the mux is correct).
Expected output (with deferred form — quiet log):
# Simulation complete.
# No assertion failures reported.
# (Coverage of mux scenarios — see cover statements)Expected output (with the BAD regular-form alternative):
# *E,ASRTST: [10] mux output mismatch: sel=01 out=0x0000_0042 in0=0x...
# *E,ASRTST: [15] mux output mismatch: sel=01 out=0x0000_0042 in0=0x...
# *E,ASRTST: [20] mux output mismatch: sel=10 out=0x0000_0073 in0=0x...
# ... 247 more transient failures ...
#
# Total errors: 250
# (None of them are real bugs — every one is a mid-transition glitch)The cost-benefit of using the deferred form is overwhelming for combinational checks: signal-to-noise improves from "buried in 250 false positives" to "every reported failure is a real bug."
Common Pitfalls
Pitfall 1 — assert #0 inside a clocked block doesn't fix the NBA race
always_ff @(posedge clk) begin
count <= count + 1;
assert #0 (count <= DEPTH); // STILL sees pre-update count
endFix: for the NBA-race problem (Module 12.2 Debug Lab), assert #0 inside the same clocked process is NOT the fix — the assertion still runs at the wrong moment relative to the NBA. Use one of: (a) check the post-update expression directly assert ((count + 1) <= DEPTH);, or (b) move the assertion to a separate always_comb block that re-evaluates when the NBA-settled count changes.
Pitfall 2 — Forgetting that always @(*) re-evaluates on input transitions
// The checker is in always @(*) — fires on every input change.
// `assert #0` defers within EACH evaluation, not across evaluations.
// If inputs change rapidly, the assertion still runs many times — but
// each time it sees a settled value, so each evaluation is correct.
// The "noise" here isn't false positives — it's repeated evaluation overhead.Fix: the assert #0 form correctly produces no false positives, but it does still re-evaluate on every input transition. For genuine performance concerns (rare), gate the always block with a settled-window condition or use a concurrent assertion at a sampling event (Module 12.4) instead.
Pitfall 3 — Using assert final when assert #0 would do
always @(*) begin
assert final (out == (a & b));
// For pure-RTL combinational checks, #0 is sufficient.
// `final` waits past the Reactive region too — unnecessary if
// there's no program-block / Reactive code modifying signals.
endFix: default to assert #0 for RTL combinational checks. Reserve assert final for environments with program blocks or Reactive callbacks that modify signals the assertion depends on. The wrong-form choice isn't incorrect — it just signals "the engineer wasn't sure which region to use."
Pitfall 4 — Treating deferred assertions as a panacea
// X-propagation example:
always @(*) begin
assert #0 (out !== 1'bx);
// If 'out' is X at the end of the Observed region — because an
// upstream signal is X — the assertion correctly fires. But the
// underlying cause (the X-propagating signal) is upstream of
// this check. The deferred form doesn't hide X failures; it just
// ensures the X is real (settled) and not transient.
endFix: deferred assertions solve the glitch / mid-cycle false-positive problem. They do not solve X-propagation, design-logic bugs, or wrong-bin coverage. Use the deferred form when the symptom is "fires during transitions but the settled value is correct" — not as a universal noise filter.
Debug Lab — "I Switched to assert #0 and Some Failures Disappeared but Not All"
A real failure mode: the engineer applies assert #0 to clean up combinational-glitch noise; some failures stop firing (the glitches), but other failures remain — and the engineer assumes the remaining failures are also false positives.
module decoder_with_checker (
input bit [2:0] in,
output bit [7:0] out
);
// BUG: only one bit set per legal input, but unused 'in' values
// produce out=0 (which the spec considers illegal)
always_comb begin
case (in)
3'b000: out = 8'b0000_0001;
3'b001: out = 8'b0000_0010;
3'b010: out = 8'b0000_0100;
3'b011: out = 8'b0000_1000;
// Cases 3'b100..3'b111 fall through — out stays at 0
// (spec says: every legal in produces exactly one-hot out)
endcase
end
// First attempt — regular immediate (noisy false failures):
// always @(*) assert ($onehot(out)) else $error("not onehot");
//
// → Engineer sees 100+ failures, blames glitches, switches to #0
// Second attempt — deferred:
a_onehot:
always @(*) begin
assert #0 ($onehot(out))
else $error("[%0t] decoder out=%b not one-hot (in=%b)",
$time, out, in);
end
endAfter switching from regular to assert #0, the failure count drops from 250+ to 8. The engineer concludes "I cleaned up the noise" and considers the remaining 8 failures as more glitches. The verification lead reviews the assertion log and asks: "Why does the failure-list show in=3'b100, in=3'b101, in=3'b110, in=3'b111? Those aren't transitions — those are spec-legal input values." The engineer realizes the remaining 8 failures are real bugs: the decoder doesn't handle inputs 4–7. The spec says every legal in value must produce a one-hot output; the RTL silently produces out=0 for those inputs.
The deferred form correctly suppressed the 240+ transient glitches that the regular form was reporting as false positives. But the remaining 8 failures were never glitches — they were real spec violations in the case statement. The engineer's mental model was "assert #0 filters noise" — and applied that model to the remaining failures without inspecting them. The deferred form did its job correctly; the engineer's interpretation of "what disappeared" did not extend to a careful re-reading of "what remains."
The deeper lesson is that assert #0 doesn't filter real failures — it only filters transient ones. After switching to the deferred form, every reported failure should be assumed real until proven otherwise. The deferred form is a noise reducer, not a noise eliminator.
Fix the RTL — extend the case statement to handle every legal in value. The assertion was correctly reporting a real spec violation.
always_comb begin
case (in)
3'b000: out = 8'b0000_0001;
3'b001: out = 8'b0000_0010;
3'b010: out = 8'b0000_0100;
3'b011: out = 8'b0000_1000;
3'b100: out = 8'b0001_0000;
3'b101: out = 8'b0010_0000;
3'b110: out = 8'b0100_0000;
3'b111: out = 8'b1000_0000;
default: out = 8'b0000_0000; // unreachable, but lint-safe
endcase
endThe discipline rule that prevents recurrence: when switching from regular to deferred assertions, treat the new failure count as the new baseline of real bugs. Don't assume "fewer failures = same kind of failures, just less noise." Inspect each remaining failure individually — the deferred form's value comes precisely from making the real-vs-noise distinction clear, and skipping that inspection forfeits the benefit.
The complementary discipline: pair the deferred assertion with a cover property (Module 12.10) that confirms every legal input value was exercised by the regression. The cover demonstrates the regression's reach; the assert demonstrates the design's correctness within that reach. Together they prove "every legal scenario was tested AND the design honored the spec while every legal scenario ran."
Industry Context — Where Deferred Assertions Live
- Combinational equivalence checkers. Mux output checks, adder/subtractor result checks, comparator validity — every shipping arithmetic-IP VIP uses deferred assertions for its combinational invariants. Regular immediate would flood the log with mid-cycle transition false positives.
- One-hot /
$onehot0validity checks on bus arbiters. Insidealways_comb, deferred assertions verify that the grant vector is one-hot at the end of every Observed region — catching real arbitration bugs while ignoring transient hazards on input changes. - Spec-conformance checkers in UVM monitors. Where the monitor's combinational logic computes "expected" values from observed signals, deferred assertions on the expected-vs-observed delta give clean failure logs without per-transition noise.
- DPI-C wrapper validity. SV wrappers around C reference models compute expected outputs combinationally; deferred assertions verify the agreement with the DUT's RTL outputs. The deferred form ensures both sides have settled before comparison.
- Cross-clock-domain CDC checkers. Pre-synchronizer assertion on combinational sampling — the deferred form ensures the synchronizer's input is sampled in a stable window before assertion.
- FPGA/ASIC vendor-IP integration checks. When an integrator wraps a vendor IP and adds combinational sanity assertions on the IP's outputs, deferred assertions give the integrator clean failure logs without the transient noise from the wrapped IP's internal clock domains.
The pattern across these uses: the deferred form is the default for combinational checks in production verification environments. Regular immediate assertions are reserved for value-validity checks at clocked points, argument validation in tasks, and configuration sanity in initial blocks. The two forms cover different problems; production codebases use both with discipline.
Interview Q&A — Ten Questions You Will Be Asked
An immediate assertion whose evaluation is deferred to a later region in the same time step — either the Observed region (assert #0) or the Final region (assert final). The deferred form runs at the same $time as a regular immediate, but it sees the settled values that result after NBA (and possibly Reactive) updates apply. The motivation: regular immediate assertions on combinational outputs fire on mid-cycle glitches; the deferred form waits for the glitch to settle.
Coding Guidelines — Seven Rules for Deferred-Assertion Discipline
- Default to
assert #0for combinational checks inalways @(*)/always_combblocks. Regular immediate assertions in these contexts flood the log with mid-transition false positives; the deferred form gives clean failure logs. - Reserve
assert finalfor environments withprogramblocks or Reactive-region updates. For pure-RTL combinational checks in modern UVM environments,#0is sufficient. Usingfinaleverywhere isn't wrong but signals "the engineer wasn't sure which region applied." - Don't use
assert #0insidealways_ff @(posedge clk). The clocked block already runs in the Active region with values that are settled at the clock edge — the deferred form solves a problem you don't have. For NBA-race problems (Module 12.2), use a different fix: compute the post-update expression, or move the assertion to a separatealways_combblock. - Treat every remaining failure after switching to deferred form as real. The deferred form's value comes from making real-vs-noise distinct. Inspect each failure individually; don't dismiss them as "more residual glitches."
- Name every deferred assertion with an
a_prefix. Anonymous deferred assertions report by file:line — same readability problem as anonymous regular immediate assertions. - Format fail messages as bug reports. Include
$time, the offending values, and enough context that the verification engineer reading the log knows where, when, and with what values.$timewill be the same for regular and deferred forms — but the printed values may differ. Print both, when ambiguous. - Pair deferred assertions with cover statements to prove the regression exercised the checked scenarios. A deferred assertion that never fires might mean "no bugs" or "the scenario was never exercised." Module 12.10's
cover propertydistinguishes the two.
Summary
Deferred immediate assertions are the variant of immediate assertions that wait until after the NBA region (assert #0, Observed) or the absolute end of the time step (assert final, Final) before evaluating. They solve the false-failure problem that regular immediate assertions exhibit on combinational outputs — mid-transition glitches that look like assertion failures but aren't real bugs. The simulation-region model (Active → NBA → Observed → Reactive → Final) explains why the deferred forms see different values than regular immediate forms at the same $time. Use assert #0 for combinational checks in always @(*) / always_comb blocks; reserve assert final for environments with program blocks or Reactive-region updates. After switching to the deferred form, treat every remaining failure as real until proven otherwise — the deferred form's noise filtering ends at transients, not at real bugs. Module 12.4 introduces the concurrent assertion form, which provides continuous monitoring across clock cycles — the form that does most of the heavy lifting in real protocol verification.