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.
// 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
endtaskUnlike 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.
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.
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.
// snippet
always_ff @(posedge clk) begin
if (state == GRANT) begin
// Means: @(posedge clk) (state == GRANT) |-> ##1 busy
a_busy: assert property (##1 busy);
end
endThe assertion still spawns attempts per edge while enabled and still samples preponed values. Moving it out of the if would silently widen its antecedent.
The inferred clock from the always sensitivity list. Enclosing if conditions fold into the antecedent, and the check stays concurrent and temporal.
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.
// 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");
endtaskTool | 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. |
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.
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.
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
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 onceRun 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.