Skip to main content

Immediate Assertions

Checking that a signal has an expected value, in plain Verilog, means writing an if statement and manually deciding what to do when it's false — print an error, set a flag, stop the simulation. That's boilerplate repeated at every single check in a testbench, and it's easy to write inconsistently (one engineer's checks $display an error, another's silently sets a flag nobody reads). SystemVerilog's assert statement standardizes this into one line.

The basic form​

assert (sum == (a ^ b ^ cin))
else $error("sum mismatch: expected %0d, got %0d", a ^ b ^ cin, sum);

assert (expression) checks the expression immediately, at the point the statement executes (hence "immediate" assertion — evaluated procedurally, like an if, as opposed to the concurrent, clock-based assertions on the next page). If the expression is true, nothing happens — execution just continues; the else clause only runs if the expression is false. This replaces the equivalent hand-written if (!(...)) $display(...) with something that reads, at a glance, as "this had better be true" rather than looking like an ordinary conditional a reader has to parse to realize it's a check.

An assertion's expression fails — running the else clause — whenever it evaluates to 0, x, or z; only an expression that evaluates cleanly to a nonzero, fully-known value counts as a pass. This matters because a genuinely undriven or metastable 4-state (logic) signal reads as x, and a naive if (!(...)) written by hand might accidentally let an x slip through depending on how the comparison happens to evaluate — assert treats x/z as a failure unconditionally, with no such gap.

Severity system tasks​

assert (state != INVALID_STATE)
else $fatal(1, "FSM entered an invalid state — halting simulation");

assert (queue.size() < MAX_DEPTH)
else $warning("queue depth %0d is getting close to the limit", queue.size());
TaskSeverityEffect
$infoInformationalJust a message — no implication anything is wrong
$warningWarningA message flagging a possible issue, simulation continues
$errorErrorFlags a real failure; simulation continues (the default) but the failure is recorded/counted
$fatalFatalFlags a failure and stops the simulation immediately

Choosing the right severity matters: $error for something that's definitely wrong but where continuing the simulation might still surface more information (more mismatches downstream, say), $fatal for something so broken that continuing would only produce meaningless garbage results (an FSM stuck in an undefined state, corrupting everything downstream from that point on).

Assertions vs. an if/$display by hand​

// What every check used to look like:
if (sum !== (a ^ b ^ cin)) begin
$display("ERROR: sum mismatch");
end

// The same check as an immediate assertion:
assert (sum == (a ^ b ^ cin))
else $error("sum mismatch");

Both do the same thing functionally — but the assert version is self-documenting (any reader instantly recognizes it as a correctness check, not ordinary control flow), and most simulators additionally track a running pass/fail count of every assertion executed, giving a testbench a built-in summary of how many checks actually ran and how many failed, without the test writer building that bookkeeping by hand.

A pass action, not just a fail action​

assert (state != INVALID_STATE)
pass_count++; // runs when the expression is true
else
$error("FSM entered an invalid state"); // runs when the expression is false

The basic form's else clause isn't the only optional branch — assert (expression) pass_statement; else fail_statement; lets both outcomes run their own code, the same shape an if/else has. This is uncommon (most checks only care about the failure path), but shows up when a passing check should also update its own tally — pass_count++ above — rather than being invisible when everything is working correctly.

Deferred assertions: assert #0 and assert final​

always_comb begin
assert #0 (sum == (a ^ b ^ cin))
else $error("sum mismatch");
end

A plain assert checks its expression at the exact point the statement executes — inside an always_comb block reacting to input changes, that can be mid-glitch, before every input has actually settled for this time step, producing a false failure that has nothing to do with a real bug. A deferred immediate assertion delays the actual check to the end of the current time step instead, after every blocking and non-blocking update has settled: assert #0 (...) defers to the Observed region (after all NBA updates finish), and assert final (...) defers even later, to the very end of the time step. Deferred assertions are the safer default inside combinational logic and other code that can execute mid-update — a plain (non-deferred) immediate assertion is more appropriate only where the check point is already known to be stable, like the end of a task after a value has fully settled.

Immediate cover and assume​

cover (state == IDLE); // records how often this expression was true when executed — no pass/fail verdict
assume (rst_n || !clk); // tells a formal tool to treat this as a given, not something to prove

assert, cover, and assume all share the same basic syntax, but mean different things: assert is a check with a pass/fail verdict; cover has no verdict at all — it just records how often the expression was true whenever this line executed, contributing to assertion coverage rather than catching a bug; assume doesn't check anything in ordinary simulation (most simulators treat it as a no-op or a lightweight sanity check) — it exists primarily for formal verification tools, telling the formal engine "treat this condition as always true, and don't bother searching states where it's violated."

Naming an assertion with a label​

check_sum: assert (sum == (a ^ b ^ cin))
else $error("sum mismatch: expected %0d, got %0d", a ^ b ^ cin, sum);

An optional label: in front of assert names that specific assertion statement — check_sum here. It's easy to skip since the assertion works identically either way, but a label earns its keep the moment something goes wrong: simulator log messages, coverage/assertion-count reports, and waveform-viewer assertion browsers all identify a failure by this name rather than by file-and-line-number alone, which matters once a testbench has dozens of assertions and a failure report needs to point unambiguously at which one fired. Labeling every assertion — not just the ones that happen to need it today — is the cheaper habit; adding one retroactively after a confusing debug session is the expensive way to learn the same lesson.

Where immediate assertions fit​

Immediate assertions are best suited to checks that make sense at one specific point in procedural code — right after a value should have settled, inside a function, at the end of a task. They cannot express a check that spans multiple clock cycles ("if A is true this cycle, B must become true within the next 3 cycles") — that requires the temporal, clock-relative constructs on the next page: concurrent assertions.

What's next​

The next page covers concurrent assertions (SystemVerilog Assertions / SVA) — assert property, sequences, and the temporal operators that check behavior across clock cycles, not just at one instant.