Part 4 · Assertions (SVA) · Intermediate

expect, Procedural Concurrent Assertions

Blocking property waits with expect in tasks, concurrent assertions inside always blocks with inferred clocks, and expect vs event waits.

expect — a blocking wait on a property

expect takes the same property syntax as assert property, but it lives in procedural code (tasks, initial blocks) and it blocks: execution stops, ONE evaluation attempt of the property starts at the next clock edge, and the statement completes when that single attempt passes or fails. It is the bridge between directed-testbench thinking ("now wait for the response") and SVA's temporal language.

systemverilog
task automatic do_read (input logic [7:0] addr, output logic [31:0] data);
  @(posedge clk);
  rd_en   <= 1'b1;
  rd_addr <= addr;
  @(posedge clk);
  rd_en   <= 1'b0;

  // Block here until the response property completes (pass or fail)
  expect (@(posedge clk) ##[1:10] rsp_valid)
    data = rsp_data;                       // pass action
  else begin
    $error("read response timeout for addr %0h", addr);
    data = 'x;                              // fail action
  end
endtask

Unlike assert property, which launches a new attempt every clock edge for the whole simulation, expect launches exactly one attempt, beginning at the next clock edge after the statement is reached. There is no antecedent and no vacuous pass — the property simply must match within its window or the else branch runs.


Cycle picture: one attempt, then move on

diagram
expect (@(posedge clk) ##[1:4] rsp_valid)

cycle      :   0     1     2     3     4     5
clk        :   ┌─┐___┌─┐___┌─┐___┌─┐___┌─┐___┌─┐
task time  :   reaches expect at cycle 0 (mid-cycle)
attempt    :         ◄── single attempt starts at next edge (cycle 1)
rsp_valid  :   0     0     0     1     -     -

  cycle 1:  ##1 window opens, rsp_valid=0  keep waiting
  cycle 2:  rsp_valid=0  keep waiting
  cycle 3:  rsp_valid=1  attempt MATCHES  pass action runs,
            task resumes immediately after cycle 3 edge

Compare assert property: would start a NEW attempt at every edge
(cycles 1, 2, 3, 4, ...) forever. expect = one shot, then code resumes.

Because expect uses sampled (preponed) values like every concurrent construct, the values it tests are those just before each clock edge — consistent with assertions, but one cycle earlier than a naive @(posedge clk) read in the same task would see if signals are driven on the edge.


Concurrent assertions inside always blocks

An assert property written inside an always block is still a concurrent assertion, but it infers context from the surrounding code: the clock comes from the always sensitivity, and any enclosing if conditions become part of the antecedent. This keeps an assertion next to the logic it guards.

systemverilog
always_ff @(posedge clk) begin
  if (state == GRANT) begin
    // Inferred: @(posedge clk) (state == GRANT) |-> ##1 busy
    a_busy: assert property (##1 busy)
      else $error("busy did not follow GRANT");
  end
  // ... normal sequential logic continues ...
end

// Exactly equivalent stand-alone form:
a_busy_eq: assert property (
  @(posedge clk) (state == GRANT) |-> ##1 busy);

Inference rules

  1. Clock: taken from the always block's event control — @(posedge clk) here.

  2. Enabling condition: every enclosing if/case guard is conjoined into the antecedent of an implication.

  3. The assertion still evaluates with sampled values and still spawns attempts per clock edge while enabled — it is NOT an immediate assertion.

  4. Deferred/immediate assertions (assert final / assert #0) inside procedures are a different construct — they check a boolean now, with no temporal window.


Checker-style usage in testbench sequences

expect shines in directed phases of class-based testbenches: init sequences, register bring-up, error-injection scenarios — places where the stimulus code itself knows a temporal response is due and wants to fail loudly, locally, and with context.

systemverilog
task automatic power_up_sequence();
  apply_reset();

  // PLL must lock within 100 cycles of reset release
  expect (@(posedge clk) ##[1:100] pll_locked)
    $display("PLL locked at %0t", $time);
  else $fatal(1, "PLL never locked after reset");

  // Calibration handshake: start, then done with busy held throughout
  cal_start <= 1'b1;
  expect (@(posedge clk) ##1 (cal_busy throughout cal_done [->1]))
  else $error("calibration dropped busy before done");
endtask

expect vs waiting on events — when to use which

  • wait (sig) / @(posedge sig): unbounded, no timeout, no temporal shape — fine for simple synchronization, silent hang if the signal never comes.

  • expect with ##[1:N]: bounded wait with a built-in timeout and a fail branch — the hang becomes an immediate, attributable error.

  • expect with full sequence syntax: can demand a SHAPE (busy throughout done [->1]), which an event wait cannot express at all.

  • assert property: for invariants that must hold on every cycle of every test — not for one-shot waits inside stimulus.

  • Rule of thumb: stimulus that waits for a bounded temporal response → expect; permanent protocol rule → assert property; trivial sync → @(posedge).


Interview angle

Two standard probes. First: "What is the difference between expect and assert property?" — assert property is declarative and starts an attempt every clock edge for the whole run; expect is procedural, blocks the calling thread, and runs exactly one attempt starting at the next edge. Second: "What clock does an assertion inside an always block use?" — the inferred clock from the always sensitivity list, and enclosing if conditions are folded into the antecedent. A strong answer also volunteers the trap: expect still samples preponed values, so it sees the same values an assertion would — not the values a blocking read in the task would see after the edge.

Key takeaways

  • expect = procedural, blocking, single evaluation attempt of a property — pass/else branches like an if.

  • assert property = declarative, an attempt every clock edge, forever; use it for invariants, expect for stimulus waits.

  • Assertions inside always blocks infer clock from the sensitivity list and antecedent from enclosing if guards.

  • expect converts silent testbench hangs into bounded, attributable failures.

  • All of these sample preponed values — consistent with assertion semantics, not with post-edge procedural reads.

Common pitfalls

  • Using wait(sig) for a response that can be lost — the test hangs with no message; expect with a bounded range fails cleanly.

  • Expecting expect to retry: it runs one attempt; if the property can start at multiple alignments you must loop yourself.

  • Assuming an in-always assertion is immediate — it is concurrent, temporal, and samples preponed values.

  • Forgetting the inferred if-condition: moving the assertion out of the if block silently widens its antecedent.

  • Driving a signal and expecting it in the same edge — preponed sampling means the attempt sees the old value.