Skip to content

SystemVerilog · Module 12

Immediate Assertions

The procedural, same-instant SVA form — used inside always blocks, tasks, and functions. The three forms (fail-only, pass+fail, bare), the four severity levels ($fatal/$error/$warning/$info), placement rules, the Active-region race-condition gotcha that motivates deferred assertions, and the canonical FIFO-checker pattern.

Module 12 · Page 12.2

What an Immediate Assertion Is

An immediate assertion is the procedural form of SVA: it lives inside an always block, an initial block, a task, or a function, and it evaluates the moment execution reaches the assert line — exactly like an if statement. There is no clock involvement, no temporal window, no waiting. The condition is checked at the simulation instant the procedural code runs it. If the condition is true, the assertion passes (optionally running a pass action); if false, the fail action runs (typically $error or $fatal).

Immediate assertions are the right tool when the question you need to answer is "is this value valid right now?" — argument range checks, FIFO count bounds at the moment of a push/pop, configuration sanity at simulation startup. They are not the right tool when the question is "did this temporal contract hold across cycles" — that's what Module 12.4's concurrent assertions are for.

Syntax — Three Forms

The immediate assertion has one structural shape and three useful subforms.

SystemVerilog — immediate assertion grammar
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── General form ─────────────────────────────────────────────────
assert ( boolean_expression )
    pass_statement                  // optional — runs if expr is TRUE
else
    fail_statement;                 // optional — runs if expr is FALSE

Both the pass and the fail statements are optional. That gives three practically useful subforms.

Form 1 — fail-only (the most common)

SystemVerilog — fail-only immediate assertion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always @(posedge clk) begin
    if (write_enable)
        assert (addr < MEM_SIZE)
            else $error("Address out of bounds: 0x%0h", addr);
end

What this does. Every rising clock edge, if write_enable is high, the simulator evaluates addr < MEM_SIZE. If the comparison is true, the assertion silently passes. If false, $error runs — logging the offending address with $time context, incrementing the simulator's error counter, but letting the simulation continue (per $error semantics; see §3 below). This is the workhorse form — 95% of production immediate assertions are fail-only.

Form 2 — pass + fail (rare but useful for tracing)

SystemVerilog — pass + fail immediate assertion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always @(posedge clk) begin
    assert (data_valid)
        $display("[PASS] Data valid: 0x%0h at %0t", data, $time)
    else
        $error("[FAIL] Data invalid at time %0t", $time);
end

When to use the pass action. Rarely — its presence creates simulation noise on every passing cycle, which buries real signals. Two legitimate uses: (1) $info-based tracing during interactive debug runs; (2) coverage helpers where each pass is meant to be counted (though Module 11's cover property is usually the better tool for this).

Form 3 — bare assert

SystemVerilog — bare immediate assertion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assert (fifo_count <= MAX_COUNT);

What this does. No pass action, no fail action — the simulator emits its default error message on failure. Useful for one-off sanity checks where the default message ("Assertion failed at <file>:<line>") is informative enough. Production assertions almost always use Form 1 with a meaningful $error message; bare asserts appear mostly in debug-time scratch code.

The Four Severity Levels

The fail statement can use four severity system tasks. The severity tells the simulator how to respond and tells the verification engineer reading the log how serious the failure is.

System taskEffect on simulationCounter incrementedUse when
$fatal(N, msg)Stops simulation immediatelyFatalThe design is in an impossible state; continuing is meaningless
$error(msg)Continues; logs the errorErrorSpec violation or real design bug — the canonical fail action
$warning(msg)Continues; logs the warningWarningSuspicious-but-not-definitely-wrong condition
$info(msg)Continues; logs the info(none)Debug tracing, expected-condition confirmations
SystemVerilog — all four severity levels in context
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── $fatal — impossible state; stop everything ───────────────────
assert (count <= DEPTH)
    else $fatal(1, "count=%0d exceeds DEPTH=%0d — impossible!",
                  count, DEPTH);
// The first argument (1) is the simulator finish code. 0 = no error code;
// non-zero = error. $fatal halts the simulator with that code.
 
// ── $error — protocol violation; continue and count ──────────────
always @(posedge clk) begin
    if (push && !pop)
        assert (count < DEPTH)
            else $error("[%0t] FIFO overflow! push when count=%0d == DEPTH",
                         $time, count);
end
 
// ── $warning — suspicious but legal ──────────────────────────────
assert (latency <= MAX_LATENCY)
    else $warning("[%0t] Latency=%0d approaching spec maximum of %0d",
                   $time, latency, MAX_LATENCY);
 
// ── $info — informational only ───────────────────────────────────
assert (state == IDLE)
    $info("[%0t] FSM returned to IDLE", $time)
    else $info("[%0t] FSM not in IDLE — state=%0d", $time, state);

The severity-selection rule. Match the severity to what the violation means for the design:

  • Use $fatal only when continuing the simulation cannot produce meaningful results — counter exceeded its width, pointer to an unallocated object, configuration parameter outside the synthesisable range.
  • Use $error for real bugs — protocol violations, FSM contract failures, FIFO overflow/underflow. The simulation continues so other assertions in the same run can also surface their failures; the regression report rolls them all up.
  • Use $warning for conditions that are legal per spec but worth flagging — latency near the spec maximum, queue depth near capacity, a corner case that succeeded once.
  • Use $info for expected tracing — useful in interactive debug runs, silenced in regression runs via verbosity controls.

The wrong severity choice is one of the most common ways production assertion files become unreadable. $fatal on a routine protocol violation stops the simulation before other assertions can fire; $error on every passing condition floods the log; $warning on a real bug means the regression triage misses the failure entirely.

Where to Place Immediate Assertions

Immediate assertions are procedural statements. They live anywhere procedural code lives.

ContextFiresTypical use
always @(posedge clk)Every rising clock edgeFIFO overflow/underflow, protocol checks
always_comb / always @(*)Whenever any input changesCombinational validity, mutex constraints
initial blockStart of simulation (or at scheduled times)Configuration sanity, parameter validation
Task bodyWhen the task is calledArgument validation, pre/post conditions
Function bodyWhen the function is calledArgument range checks, return-value invariants
Inside if / caseWhen that branch is takenContext-specific checks
SystemVerilog — placement contexts in practice
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Inside an always @(posedge clk) — runtime per-cycle check ────
always @(posedge clk) begin
    if (write_enable)
        assert (write_addr < MEM_DEPTH)
            else $error("Write addr 0x%0h >= depth 0x%0h",
                         write_addr, MEM_DEPTH);
end
 
// ── Inside always_comb — combinational validity ─────────────────
always_comb begin
    // mutex check — only one grant should be high at any time
    assert ($onehot0({grant_a, grant_b, grant_c}))
        else $error("[%0t] Multiple grants asserted: %b%b%b",
                     $time, grant_a, grant_b, grant_c);
end
 
// ── Inside initial — configuration sanity ──────────────────────
initial begin
    assert (DATA_WIDTH > 0 && DATA_WIDTH <= 128)
        else $fatal(1, "DATA_WIDTH=%0d outside synthesisable range",
                     DATA_WIDTH);
    assert (DEPTH inside {[1:1024]})
        else $fatal(1, "DEPTH=%0d outside valid range", DEPTH);
end
 
// ── Inside a task — argument validation ─────────────────────────
task automatic write_register(input bit [31:0] addr, bit [31:0] data);
    assert (addr inside {VALID_REG_RANGES})
        else $error("[%0t] Invalid register addr 0x%0h", $time, addr);
    // ... actual write logic ...
endtask
 
// ── Inside a function — precondition check ─────────────────────
function automatic int compute_offset(input int idx);
    assert (idx >= 0)
        else $error("Negative idx=%0d passed to compute_offset", idx);
    return idx * 4;
endfunction

The placement-selection rule. Place the assertion at the point in the procedural flow where you want the check to run. An assertion inside an if branch only fires when that branch is taken; an assertion at the top of a task fires on every call regardless of the caller's context. The placement is the contract about when the check applies.

A Complete Working Example — FIFO Checker

The pattern below is the canonical immediate-assertion FIFO checker. Every push/pop pair is validated against the count-vs-depth contract; the assertion fires the moment a contract violation occurs.

SystemVerilog — FIFO checker with immediate assertions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    parameter int DEPTH = 16,
    parameter int WIDTH = 8
) (
    input  bit             clk, rst,
    input  bit             push, pop,
    input  bit [WIDTH-1:0] din,
    output bit [WIDTH-1:0] dout,
    output bit             full, empty
);
    bit [WIDTH-1:0] mem [DEPTH];
    int unsigned    count;
    int unsigned    wr_ptr, rd_ptr;
 
    assign full  = (count == DEPTH);
    assign empty = (count == 0);
 
    always_ff @(posedge clk or posedge rst) begin
        if (rst) begin
            count  <= 0;
            wr_ptr <= 0;
            rd_ptr <= 0;
        end else begin
            // ── Contract 1: never push when full ──────────────────
            if (push && !pop) begin
                assert (count < DEPTH)
                    else $error("[%0t] FIFO overflow! push when count=%0d == DEPTH=%0d",
                                 $time, count, DEPTH);
                mem[wr_ptr] <= din;
                wr_ptr      <= (wr_ptr + 1) % DEPTH;
                count       <= count + 1;
            end
 
            // ── Contract 2: never pop when empty ──────────────────
            else if (pop && !push) begin
                assert (count > 0)
                    else $error("[%0t] FIFO underflow! pop when count=0",
                                 $time);
                dout   <= mem[rd_ptr];
                rd_ptr <= (rd_ptr + 1) % DEPTH;
                count  <= count - 1;
            end
 
            // ── Contract 3: simultaneous push+pop preserves count ─
            else if (push && pop) begin
                mem[wr_ptr] <= din;
                dout        <= mem[rd_ptr];
                wr_ptr      <= (wr_ptr + 1) % DEPTH;
                rd_ptr      <= (rd_ptr + 1) % DEPTH;
                // count unchanged — verify the flag invariant holds
                assert ((count > 0 && count < DEPTH) ||
                        (count == 0 && !empty) ||
                        (count == DEPTH && !full))
                    else $error("[%0t] Push+pop with inconsistent flags: count=%0d full=%b empty=%b",
                                 $time, count, full, empty);
            end
        end
    end
 
    // ── Contract 4: full and empty are mutually exclusive ────────
    always_comb begin
        assert (!(full && empty))
            else $error("[%0t] Inconsistent FIFO flags: full=1 AND empty=1",
                         $time);
    end
 
    // ── Contract 5: count and flag invariants ────────────────────
    always_comb begin
        assert (full == (count == DEPTH))
            else $error("[%0t] full flag mismatch: full=%b count=%0d",
                         $time, full, count);
        assert (empty == (count == 0))
            else $error("[%0t] empty flag mismatch: empty=%b count=%0d",
                         $time, empty, count);
    end
endmodule

What this code does. Defines a parametric FIFO module with five immediate-assertion contracts: no push when full, no pop when empty, flag consistency on simultaneous push+pop, mutex between full and empty, and count-vs-flag invariants. The first three live inside the clocked process (run on each clock edge when their guards fire); the last two live in always_comb (run whenever the relevant signals change).

How to simulate it. Compile and instantiate the module in a testbench that drives push/pop sequences. Run a regression that includes overflow, underflow, and rapid push/pop sequences. Observe the assertion log; every violation reports with $time, the offending count, and the contract that was breached.

Expected output (fragment from a failing run).

Simulation log fragment
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# *E,ASRTST: [125] FIFO overflow! push when count=16 == DEPTH=16
#    Assertion fired in fifo.sv:32
# *E,ASRTST: [310] FIFO underflow! pop when count=0
#    Assertion fired in fifo.sv:42
 
# Simulation summary:
#   Errors:  2
#   Warnings: 0
#   Assertions failed: 2 of 5 contracts

What to read out of this. Two contracts violated, two specific times, two specific counts. The verification engineer reading this log knows immediately: at time 125, the testbench attempted a push when the FIFO was already at DEPTH; at time 310, the testbench attempted a pop when the FIFO was empty. The waveform-walking step is eliminated — both root causes are localised by the assertion log directly.

Common Pitfalls

Pitfall 1 — Race condition between assertion and NBA update

SystemVerilog — assertion races the NBA in the same always block
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_ff @(posedge clk) begin
    count <= count + 1;
    // BUG: this assertion checks `count` BEFORE the NBA update happens
    // — so it always sees the old value, not the new one
    assert (count <= MAX);
end

Fix: check the value that will result from the update — assert ((count + 1) <= MAX); — or move the assertion to a separate always_comb block that sees the settled value. Module 12.3 introduces deferred immediate assertions, which solve this class of race directly.

Pitfall 2 — Forgetting that immediate assertions don't "watch" signals

SystemVerilog — immediate doesn't monitor; it checks on execution
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    assert (rst_n === 1'b1);    // checks ONCE, at time 0
    #1000;
    // The simulator is NOT continuously watching rst_n. If rst_n drops
    // at time 500, this assertion does not fire — it already ran at time 0.
end

Fix: for continuous monitoring of a signal, use a concurrent assertion (assert property (@(posedge clk) rst_n === 1'b1);). Immediate assertions are snapshots — they fire once at the point of execution. Module 12.4 onward covers the continuous-monitoring form.

Pitfall 3 — $fatal everywhere

SystemVerilog — $fatal halts the regression at the first bug
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assert (...) else $fatal(1, "addr out of range");
assert (...) else $fatal(1, "data invalid");
assert (...) else $fatal(1, "FSM in illegal state");
// The first failing assertion stops the simulation.
// The next two bugs in the same run are never reported.
// Five real bugs → five sequential regression runs to find them.

Fix: use $error for protocol violations and design bugs; reserve $fatal for genuinely impossible states. The regression then surfaces every bug in one run.

Pitfall 4 — Naming-free assertions

SystemVerilog — anonymous assertions are unreadable in logs
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assert (cond1) else $error("err1");
assert (cond2) else $error("err2");
// Simulator log shows:
//   Assertion at fifo.sv:42 failed
//   Assertion at fifo.sv:47 failed
// Grep-unfriendly; refactor-fragile (line numbers move).
 
// Better — name every assertion:
a_no_overflow:
assert (cond1) else $error("FIFO overflow");
a_no_underflow:
assert (cond2) else $error("FIFO underflow");

Fix: name every assertion with an a_* prefix. The log entries become grep-friendly, the names survive line-number drift, and the verification-plan citation lives next to the name.

Debug Lab — "The Assertion Checks the Wrong Value"

A real failure mode that catches every team's first interaction with immediate assertions inside clocked blocks.

Buggy code
SystemVerilog — assertion checks pre-NBA value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_ff @(posedge clk or posedge rst) begin
    if (rst) begin
        count <= 0;
    end else begin
        if (push)
            count <= count + 1;
        if (pop)
            count <= count - 1;
 
        // BUG: this assertion sees `count` BEFORE the NBA updates apply
        // — it checks the value count had at the START of this time step
        assert (count <= DEPTH)
            else $error("count exceeded DEPTH");
    end
end
Symptom

The FIFO is parametrised with DEPTH = 16. The testbench pushes 17 items without popping. The expected behaviour is: the assertion fires on the 17th push (when count would become 17). What actually happens: the assertion never fires, because on the 17th push, the assertion checks count — which at the moment of the assertion's evaluation is still 16 (the NBA update to 17 hasn't been applied yet). The simulator continues. The next push attempt is at count=17 — but by then, the rest of the design has accepted the overflow as legal, and the bug has propagated.

Root cause

Immediate assertions execute in the Active region of the simulator's scheduling model — the same region the procedural code lives in. When count <= count + 1 runs, the NBA update is scheduled for the end of the time step (the NBA region). The assertion that follows in the same always_ff block runs before the NBA region; it sees count at its pre-update value.

The assertion is syntactically correct but semantically checking the wrong value. The check "is count ≤ DEPTH after this update" requires either checking the post-update value explicitly (count + 1 <= DEPTH) or moving the assertion to a region that sees the settled state.

Fix

Three valid fixes, depending on the intent.

Fix A — check the post-update value explicitly:

SystemVerilog — Fix A: check the will-be value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_ff @(posedge clk) begin
    if (push && !pop)
        assert ((count + 1) <= DEPTH)
            else $error("Pushing would overflow: count=%0d DEPTH=%0d",
                         count, DEPTH);
    // ...
end

Fix B — move the assertion to a separate always_comb that sees the settled value:

SystemVerilog — Fix B: assert in a separate combinational block
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_comb begin
    assert (count <= DEPTH)
        else $error("count=%0d exceeded DEPTH=%0d", count, DEPTH);
end

Fix C — use a deferred immediate assertion (Module 12.3 covers this):

SystemVerilog — Fix C: deferred assertion runs in Postponed region
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_ff @(posedge clk) begin
    // ... updates ...
    assert #0 (count <= DEPTH)
        else $error("count overflow at end of time step");
    // The #0 marks this as a deferred immediate — runs after NBAs settle
end

The discipline rule that prevents recurrence: inside a clocked process, an immediate assertion checks the pre-update value of any signal that's also being NBA-assigned in the same process. Code review verifies the intent matches: if the assertion needs the post-update value, use Fix A, B, or C. If it needs the pre-update value (e.g. "the precondition for this update was satisfied"), the form is already correct — just document the intent in a comment.

The complementary discipline: when an assertion fires late (the violation propagates further than expected before the assertion catches it), the first check is the region the assertion runs in. Stale-value checks at the assertion site are the single most common cause of immediate assertions that "don't fire when they should."

Industry Context — Where Immediate Assertions Earn Their Place

  • UVM register-model write checks. UVM's RAL (Register Abstraction Layer) inserts immediate assertions at the entry of every register-write task — argument validation, RO-field-write detection, mask-violation checks. The assertions fire on the call, before the write reaches the bus monitor.
  • DPI-C argument validation. When the testbench calls into a C/C++ reference model via DPI, immediate assertions in the SV wrapper validate argument ranges before the C function is invoked. A null pointer or out-of-range index becomes an SV assertion failure instead of a segfault in the C model — a much more debuggable outcome.
  • Configuration parameter sanity. Module-level initial blocks frequently host immediate assertions on synthesis-time parameters (assert (DEPTH inside {[1:1024]})) — catching bad integrations at simulation startup rather than at synthesis (or worse, at silicon).
  • FIFO and buffer checkers. The five-contract FIFO example above is the canonical industry shape — every shipping FIFO IP in major VIPs ships with these assertions.
  • Combinational hazard catches. $onehot0 / $onehot mutex checks inside always_comb blocks catch arbiter and priority-encoder hazards immediately; the violation is logged at the cycle of the hazard, not at a downstream symptom.
  • Assertion-based pre/post conditions in tasks. Tasks that walk a data structure or drive a protocol sequence frequently bracket the work with immediate assertions ("the pointer is non-null on entry," "the state is IDLE on exit"). The pre/post discipline turns task contracts into runtime-enforced specifications.

Immediate assertions are the simpler half of SVA. They handle the "right now" checks the design needs at thousands of points; the temporal contracts that span cycles need concurrent assertions, which Modules 12.4–12.9 build. Both are required for a complete verification environment.

Interview Q&A — Ten Questions You Will Be Asked

A procedural-form SVA assertion that evaluates right now, at the line of procedural code it appears in — like an if statement. It lives inside an always block, an initial block, a task, or a function. No clock involvement, no temporal window — the condition is checked at the simulation instant the procedural code reaches the assert line. If the condition is true, the assertion silently passes (or runs the optional pass action); if false, the fail action runs (typically $error or $fatal).

Coding Guidelines — Seven Rules for Immediate-Assertion Discipline

  1. Name every immediate assertion with an a_ prefix. Anonymous assertions report by file:line — fragile under refactors, grep-unfriendly. Named assertions trace to verification-plan rows and survive line-number drift.
  2. Format every fail message as a bug report. Include $time, the offending values, and enough context that a verification engineer reading the log at 11 PM the night before tape-out can act on it without re-running the simulation.
  3. Use $error as the default fail action. Reserve $fatal for genuinely impossible states; $warning for legal-but-suspicious; $info for debug tracing. The wrong severity hides bugs or floods the log.
  4. Inside always_ff, document whether the assertion checks pre-NBA or post-NBA state. A one-line comment prevents an entire class of "the assertion doesn't fire when expected" debugging sessions.
  5. For settled-state invariants, use always_comb. "This invariant must hold at all times" maps naturally to a combinational block that re-evaluates whenever any input changes — no NBA-race ambiguity.
  6. Place assertions next to the contract they enforce. A FIFO's overflow assertion lives in the FIFO module, not in the testbench. A UVM register-model invariant lives in the RAL package, not in the test. The placement makes the contract auditable in PR review.
  7. Pair immediate assertions with simulation-time tests that exercise their fail paths. A directed test that pushes 17 items into a DEPTH=16 FIFO confirms the overflow assertion fires; the test is the assertion's evidence. Without it, you're trusting the assertion to fire on a bug that the regression may never trigger.

Summary

Immediate assertions are SVA's procedural form — same-instant checks for value validity, range bounds, argument preconditions, and combinational invariants. The three syntactic forms (fail-only, pass+fail, bare) cover the practical spectrum; production code almost always uses Form 1 with a $time-stamped fail message. The four severity levels ($fatal, $error, $warning, $info) tell the simulator and the verification engineer how serious the failure is — picking the wrong severity is one of the most common assertion-discipline mistakes. Immediate assertions execute in the Active region, which means inside a clocked process they see the pre-NBA value of any signal being NBA-assigned in the same process — usually the right thing, occasionally a debug-hours-burn surprise. Module 12.3 builds on this with deferred immediate assertions, which run in the Postponed region and see settled values; Module 12.4 onward covers concurrent assertions, the form that provides continuous monitoring across clock cycles.