Skip to content

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.

SystemVerilog — the false-failure problem
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Both 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.

RegionWhat runs hereAssertion form that fires here
PreponedConcurrent-assertion sampling (Module 12.4)(concurrent only — Module 12.4)
Activealways_comb, assign, blocking assignments, #0 delaysRegular immediate assertions
InactiveRe-activated #0 events
NBANon-blocking assignment (<=) updates apply
Observed(after NBA settles)assert #0 evaluates here
Reactiveprogram blocks, final callbacks
Re-activeRe-activated reactive
FinalAbsolute last point in the time stepassert 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.

Within one time step — the order of operations
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

SystemVerilog — deferred assertion syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

Propertyassert #0 (Observed)assert final (Final)
RegionObserved (after NBA)Final (absolute last point)
Sees program block updates?NoYes
Sees Reactive region updates?NoYes
Performance overheadMinimalMinimal–small
Use whenCombinational settle is enoughUVM 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.

SystemVerilog — combinational mux output checker
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

What 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):

Deferred-form simulation log fragment
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# Simulation complete.
# No assertion failures reported.
# (Coverage of mux scenarios — see cover statements)

Expected output (with the BAD regular-form alternative):

Regular-form simulation log fragment — false-failure flood
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# *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

SystemVerilog — wrong tool for the wrong problem
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_ff @(posedge clk) begin
    count <= count + 1;
    assert #0 (count <= DEPTH);     // STILL sees pre-update count
end

Fix: 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

SystemVerilog — checker placement that produces noise even with #0
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

SystemVerilog — over-deferring for no benefit
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.
end

Fix: 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

SystemVerilog — deferred assertions can't fix every false-fire
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.
end

Fix: 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.

Buggy code
SystemVerilog — mixed real and false failures
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
end
Symptom

After 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.

Root cause

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

Fix the RTL — extend the case statement to handle every legal in value. The assertion was correctly reporting a real spec violation.

SystemVerilog — RTL fix to handle full input range
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
end

The 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 / $onehot0 validity checks on bus arbiters. Inside always_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

  1. Default to assert #0 for combinational checks in always @(*) / always_comb blocks. Regular immediate assertions in these contexts flood the log with mid-transition false positives; the deferred form gives clean failure logs.
  2. Reserve assert final for environments with program blocks or Reactive-region updates. For pure-RTL combinational checks in modern UVM environments, #0 is sufficient. Using final everywhere isn't wrong but signals "the engineer wasn't sure which region applied."
  3. Don't use assert #0 inside always_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 separate always_comb block.
  4. 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."
  5. Name every deferred assertion with an a_ prefix. Anonymous deferred assertions report by file:line — same readability problem as anonymous regular immediate assertions.
  6. 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. $time will be the same for regular and deferred forms — but the printed values may differ. Print both, when ambiguous.
  7. 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 property distinguishes 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.