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.
// ── General form ─────────────────────────────────────────────────
assert ( boolean_expression )
pass_statement // optional — runs if expr is TRUE
else
fail_statement; // optional — runs if expr is FALSEBoth the pass and the fail statements are optional. That gives three practically useful subforms.
Form 1 — fail-only (the most common)
always @(posedge clk) begin
if (write_enable)
assert (addr < MEM_SIZE)
else $error("Address out of bounds: 0x%0h", addr);
endWhat 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)
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);
endWhen 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
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 task | Effect on simulation | Counter incremented | Use when |
|---|---|---|---|
$fatal(N, msg) | Stops simulation immediately | Fatal | The design is in an impossible state; continuing is meaningless |
$error(msg) | Continues; logs the error | Error | Spec violation or real design bug — the canonical fail action |
$warning(msg) | Continues; logs the warning | Warning | Suspicious-but-not-definitely-wrong condition |
$info(msg) | Continues; logs the info | (none) | Debug tracing, expected-condition confirmations |
// ── $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
$fatalonly when continuing the simulation cannot produce meaningful results — counter exceeded its width, pointer to an unallocated object, configuration parameter outside the synthesisable range. - Use
$errorfor 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
$warningfor 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
$infofor 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.
| Context | Fires | Typical use |
|---|---|---|
always @(posedge clk) | Every rising clock edge | FIFO overflow/underflow, protocol checks |
always_comb / always @(*) | Whenever any input changes | Combinational validity, mutex constraints |
initial block | Start of simulation (or at scheduled times) | Configuration sanity, parameter validation |
| Task body | When the task is called | Argument validation, pre/post conditions |
| Function body | When the function is called | Argument range checks, return-value invariants |
Inside if / case | When that branch is taken | Context-specific checks |
// ── 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;
endfunctionThe 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.
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
endmoduleWhat 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).
# *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 contractsWhat 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
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);
endFix: 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
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.
endFix: 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
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
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.
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
endThe 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.
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.
Three valid fixes, depending on the intent.
Fix A — check the post-update value explicitly:
always_ff @(posedge clk) begin
if (push && !pop)
assert ((count + 1) <= DEPTH)
else $error("Pushing would overflow: count=%0d DEPTH=%0d",
count, DEPTH);
// ...
endFix B — move the assertion to a separate always_comb that sees the settled value:
always_comb begin
assert (count <= DEPTH)
else $error("count=%0d exceeded DEPTH=%0d", count, DEPTH);
endFix C — use a deferred immediate assertion (Module 12.3 covers this):
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
endThe 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
initialblocks 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/$onehotmutex checks insidealways_combblocks 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
- 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. - 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. - Use
$erroras the default fail action. Reserve$fatalfor genuinely impossible states;$warningfor legal-but-suspicious;$infofor debug tracing. The wrong severity hides bugs or floods the log. - 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. - 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. - 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.
- 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.