Part 4 · Assertions (SVA) · Intermediate

Immediate vs Concurrent Assertions

Procedural instant checks vs clocked temporal properties — syntax, deferred variants, and decision rules for where each belongs.

Two different machines

SystemVerilog has two assertion kinds that share a keyword and almost nothing else. An immediate assertion is a procedural statement: it executes when the surrounding code reaches it, evaluates its expression instantly, combinationally, with current values , and is done. A concurrent assertion is a declarative, clocked process: it samples signals on a clock edge, can describe behavior spread across many cycles , and runs for the whole simulation independent of any procedural flow. Choosing the wrong one is a classic design-review finding: an immediate assertion cannot say "ack within 3 cycles", and a concurrent assertion is overkill for "this cast succeeded".

systemverilog
// IMMEDIATE — procedural, evaluates the instant it executes
task drive_item(bus_item it);
  // classic check-the-return-value idiom
  a_cast: assert ($cast(burst_it, it))
    else $error("item is not a burst_item");
endtask

always_comb begin
  // combinational sanity check, re-evaluates on any input change
  a_onehot: assert ($onehot0(grant)) else $error("grant not one-hot-0");
end

// CONCURRENT — declarative, clocked, temporal
// "every req gets an ack within 1 to 3 cycles"
ap_req_ack: assert property (@(posedge clk) disable iff (!rst_n)
  req |-> ##[1:3] ack)
  else $error("ack missed the 3-cycle window");

Note where each lives: the immediate assertions sit inside procedural code (task, always_comb), while the concurrent assertion is a module-level declaration — it needs no surrounding process, because the clock event in the property is its process.


Immediate assertions: instant evaluation and its glitch problem

Because an immediate assertion evaluates the moment procedural code reaches it, it sees intermediate delta-cycle values . In an always_comb block, inputs may settle in several delta steps within one time slot; the assertion can fire on a transient combination that no clocked logic would ever observe. That is a false failure — annoying, and in regressions, corrosive to trust.

diagram
GLITCH PROBLEM — one time slot, several delta cycles

  time slot T (no simulated time passes between deltas):

  delta 0:  a=1, b=1   always_comb runs  assert (!(a && b)) FAILS   (transient!)
  delta 1:  b←0 settles  always_comb runs  assert passes           (final value)

  An IMMEDIATE assert fires at delta 0 — a value no flip-flop would capture.
  A DEFERRED assert (assert final) waits until the slot settles  no false fire.

Deferred immediate assertions

SystemVerilog 2009 added deferred variants to fix exactly this: assert #0 (observed deferred) postpones evaluation to the Observed region of the current time slot, and assert final postpones it to the Postponed region — after everything has settled. If a glitch caused a failure that later deltas cured, the deferred report is flushed and never printed.

systemverilog
always_comb begin
  // plain immediate — may fire on delta-cycle glitches
  a_glitchy:  assert (!(ready && error));

  // observed deferred — evaluated in Observed region, glitch-immune
  a_observed: assert #0 (!(ready && error))
    else $error("ready and error both high");

  // final deferred — evaluated in Postponed region, fully settled values
  a_final:    assert final (!(ready && error));
end

Concurrent assertions: clocked and temporal

A concurrent assertion samples its signals on the clocking event — in the preponed region , meaning values from just before the edge (covered in depth in the sampling sub-lesson). Because evaluation is pinned to clock ticks, it can express relationships across cycles: delays, windows, repetition. This is the form used for protocol rules, and the form interviews mean by default when they say "write an assertion".

diagram
CONCURRENT ASSERTION — req |-> ##[1:3] ack, cycle by cycle

  clk     : T0    T1    T2    T3    T4    T5
  req     : 0     1     0     0     0     0
  ack     : 0     0     0     1     0     0

  T1: req sampled high  attempt becomes active
        │
        ├─ T2: ack? no  (cycle 1 of window)
        ├─ T3: ack? no  (cycle 2 of window)
        └─ T4: ack sampled high  MATCH  (cycle 3, last allowed)

  Same trace, ack one cycle later (at T5)  window exhausted at T4  FAIL 
  An immediate assertion CANNOT express this — no notion of "within 3 cycles".
systemverilog
// Concurrent assertions live at module scope (or in interfaces/checkers)
module fifo_props (input logic clk, rst_n, push, pop, full, empty);

  // structural protocol rules — always-on, every cycle, whole sim
  ap_no_push_full: assert property (@(posedge clk) disable iff (!rst_n)
    !(push && full))
    else $error("push while full");

  ap_no_pop_empty: assert property (@(posedge clk) disable iff (!rst_n)
    !(pop && empty))
    else $error("pop while empty");

endmodule

Decision rules: which one where

Choose by question, not by habit

  • Checking a function/task result or a value at one instant inside procedural code → immediate (deferred variant if combinational).

  • Checking a rule that involves time — "within N cycles", "until", "held while" → concurrent, no exceptions.

  • Testbench-internal sanity ($cast success, config validity, randomize() return) → immediate.

  • RTL/interface protocol rules (handshakes, FIFO invariants, FSM legality) → concurrent, usually in an interface or bound checker module.

  • Combinational invariants in RTL (one-hot mux selects) → deferred immediate inside always_comb, or a clocked concurrent assertion if a clock is natural.

diagram
DECISION TABLE

  ┌─────────────────────────┬──────────────────────┬──────────────────────────┐
  │                         │ IMMEDIATE            │ CONCURRENT               │
  ├─────────────────────────┼──────────────────────┼──────────────────────────┤
  │ where it lives          │ procedural code      │ module/interface scope   │
  │ when it evaluates       │ when code reaches it │ every clock tick         │
  │ values used             │ current (delta-live) │ preponed (pre-edge)      │
  │ time span               │ one instant          │ many cycles              │
  │ typical home            │ testbench classes    │ RTL / interfaces / bind  │
  │ glitch sensitivity      │ yes (use #0/final)   │ no (clock-sampled)       │
  └─────────────────────────┴──────────────────────┴──────────────────────────┘

Interview angle

This is the standard SVA opener. A complete 30-second answer: immediate assertions are procedural and evaluate instantly with current values; concurrent assertions are clocked, sample in the preponed region, and express multi-cycle temporal behavior. Strong candidates volunteer two extras: the deferred variants (assert #0, assert final) that solve combinational glitching, and the placement rule — TB result checks immediate, protocol rules concurrent.

  • Trap question: "Can an immediate assertion check that ack follows req in 2 cycles?" — No; it has no clock and no temporal extent. You would have to hand-code a procedural watcher, which is exactly what concurrent assertions replace.

  • Trap question: "Why did my always_comb assertion fire when the waveform looks fine?" — delta-cycle glitch; the waveform viewer shows settled values, the immediate assert saw an intermediate delta. Answer: deferred assertion.

  • Follow-up: "Where do you put concurrent assertions for a DUT you can't modify?" — interface, or a checker module attached with bind (covered in Advanced SVA).

Key takeaways

  • Immediate = procedural + instant + current values; concurrent = declarative + clocked + temporal.

  • Deferred immediate assertions (#0 / final) exist to suppress delta-cycle glitch false-fires in combinational code.

  • Temporal rules (windows, until, held-while) are impossible in immediate assertions — reach for concurrent.

  • TB sanity checks → immediate; protocol rules → concurrent in interfaces or bound checkers.

Common pitfalls

  • Plain immediate assert inside always_comb — fires on delta glitches the hardware never sees.

  • Forgetting that assert(randomize()) is an immediate assertion — with assertions disabled by tool switches, the randomize() call itself may be optimized away on some tools.

  • Writing a procedural loop with #10 delays to check a handshake window — fragile hand-rolled replica of ##[1:3].

  • Putting concurrent assertions inside class-based testbench code — they are illegal in classes; they belong in modules, interfaces, or checkers.