Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
    • SVA Fundamentals
    • Sequences: Temporal Building Blocks
    • Implication and Property Operators
    • Sampled Value Functions
    • Protocol Assertion Cookbook
    • Advanced SVA: Local Variables, Multiclock & More
      • Local Variables in Sequences
      • Multi-Clock Assertions
      • expect, Procedural Concurrent Assertions
      • Checkers and Assertion Control
    • Assertion Debug & Methodology
    • SVA Interview Drills
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Advanced SVA: Local Variables, Multiclock & More
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Assertions — Interview-Critical Deep Dive
  4. Advanced SVA: Local Variables, Multiclock & More
  5. expect, Procedural Concurrent Assertions

Part 4 · Assertions (SVA) · Intermediate

expect, Procedural Concurrent Assertions

One-shot blocking waits with expect and assertions inside always blocks.

On this page

    Why this matters

    A sequence waited for a response with a hand-written loop and timed out at the wrong cycle. An expect statement states the same wait with assertion timing.

    After this lesson you can: use expect for one blocking check in procedural code, and write assertions inside always blocks with an inferred clock.

    One attempt, then resume

    expect takes the same property syntax as assert property, but it lives in procedural code and it blocks. Execution stops and one evaluation attempt starts at the next evaluation of its clocking event.

    The statement completes when that single attempt passes or fails, then the task resumes. The examples here have no antecedent, so they cannot pass vacuously. If you give expect an implication, a false antecedent passes it vacuously and the task resumes at once.

    The task below is not run in this course's checks: it is a task fragment, Verilator 5.048 reports expect as unsupported, and Icarus 13 does not parse it.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // not-run: task fragment; Verilator 5.048 reports expect as unsupported, Icarus 13 does not parse it
    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 single response attempt completes.
      // The pass statement runs after the edge's updates, so read the sampled value.
      expect (@(posedge clk) ##[1:10] rsp_valid)
        data = $sampled(rsp_data);
      else begin
        $error("read response timeout for addr %0h", addr);
        data = 'x;
      end
    endtask

    Unlike assert property, which launches a new attempt every clock edge for the whole run, expect launches exactly one attempt. Because it samples preponed values, it sees what an assertion would see, not what a post-edge procedural read sees. Its pass and fail statements run after the edge, like action blocks, so they read current values; use $sampled there to report what the property matched.

    The single attempt, drawn

    Watch waiting against rsp_valid. One window opens, one answer closes it.

    • Slot 0: the task has reached expect and waits. rsp_valid is low.

    • Slot 1: the single attempt is open. rsp_valid is still low.

    • Slot 2: still waiting. An assert would have started three attempts by now.

    • Slot 3: rsp_valid rises. The attempt matches and the pass action runs.

    • Slot 4: waiting drops. The task has resumed.

    • Slot 5: rsp_valid drops after being consumed.

    Assertions inside always blocks

    An assert property written inside an always block is still concurrent. It infers its clock from the sensitivity list, and each enclosing if condition joins the antecedent.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet
    always_ff @(posedge clk) begin
      if (state == GRANT) begin
        // Means: @(posedge clk) (state == GRANT) |-> ##1 busy
        a_busy: assert property (##1 busy);
      end
    end

    The assertion still spawns attempts per edge while enabled and still samples preponed values. Moving it out of the if would silently widen its antecedent.

    Check yourself What clock does an assertion inside an always block use?

    The inferred clock from the always sensitivity list. Enclosing if conditions fold into the antecedent, and the check stays concurrent and temporal.

    Which wait to use

    The calibration task below is not run in this course's checks for the same reasons: it is a fragment, and neither free tool runs expect.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // not-run: task fragment; Verilator 5.048 reports expect as unsupported, Icarus 13 does not parse it
    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");
    
      // Start calibration, then demand busy held throughout done.
      cal_start <= 1'b1;
      expect (@(posedge clk) ##1 (cal_busy throughout cal_done [->1]))
      else $error("calibration dropped busy before done");
    endtask

    Tool

    Behavior

    Reach for it when

    wait, @(posedge sig)

    Unbounded wait with no timeout and no shape

    Simple synchronization that cannot hang.

    expect with ##[1:N]

    Bounded wait with a fail branch

    Stimulus waits on a temporal response.

    assert property

    An attempt every edge for the whole run

    An invariant that must always hold.

    Common pitfall

    Using wait(sig) for a response that can be lost hangs the test with no message. expect with a bounded range turns the hang into an immediate, attributable failure.

    Interview

    The two standard probes.

    • expect versus assert property: procedural and blocking with one attempt, versus declarative with an attempt every edge.

    • Clock inside always: inferred from the sensitivity list, with enclosing if conditions in the antecedent.

    • Sampling trap: expect sees preponed values, like an assertion, not post-edge procedural values.

    Check yourself 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 clocking event.

    Testbench

    The listing below is the expanded form of the in-always assertion above, written standalone so it runs here. Three grant pulses prove the shape, an idle phase proves it stays quiet otherwise, and one planted grant that busy ignores must be reported.

    grant_busy.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    module grant_busy (
      input  logic       clk,
      input  logic       rst_n,
      input  logic       bug,        // planted: busy stays low after a grant
      input  logic [1:0] state,
      output logic       busy
    );
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) busy <= 1'b0;
        else busy <= (state == 2'd1) && !bug;
    endmodule
    
    module grant_busy_checks (
      input  logic       clk,
      input  logic       rst_n,
      input  logic [1:0] state,
      input  logic       busy
    );
      int n_fail = 0;
      function automatic void fail(string msg);
        n_fail++;
        $display("[%0t] %m: %s", $time, msg);
      endfunction
      // Expanded form of: always_ff @(posedge clk) if (state == GRANT)
      //   a_busy: assert property (##1 busy);
      a_busy: assert property (@(posedge clk) disable iff (!rst_n)
        (state == 2'd1) |-> ##1 busy)
        else fail("busy did not follow GRANT");
    endmodule
    
    Terminal
    3 grants and 2 other states: busy followed every grant, 0 reports
    [155000] tb_grant_busy.mon.fail: busy did not follow GRANT
    a grant with busy stuck low: 1 report
    PASS: all 3 grant pulses produced busy, and the stuck busy was reported once

    Remember

    • expect blocks the thread and runs one attempt.

    • Assertions inside always blocks infer the clock and fold enclosing ifs into the antecedent.

    • Both see preponed values.

    Rule of thumb: use expect for one wait in a sequence; use assert property for a rule that always holds.

    How this was checked

    Run with Verilator 5.048 (--binary --timing --assert, plus --coverage when a listing has a cover property) on 2026-10-18; every bench printed the PASS line shown. Verilator stops at the first $error or failing assertion without an action block, so the benches count failures in the action block. Icarus 13 has no concurrent assertions.

    Verilator 5.048 reports expect as unsupported; the bench runs the expanded form of the in-always assertion.

    Related topics

    • SVA fundamentals

    Previous

    126 / 259

    Next: Checkers and Assertion Control

    Up next

    Up nextCheckers and Assertion Control

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Local Variables in Sequences
    clkrsp_validwaiting
    A task blocking on a bounded response wait