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. Local Variables in Sequences

Part 4 · Assertions (SVA) · Intermediate

Local Variables in Sequences

Per-thread sequence locals that carry tags and data across variable latency.

On this page

    Why this matters

    A read-data check stored the expected value in one module variable. The second request in flight overwrote it, and the check compared the first response against the wrong data.

    After this lesson you can: capture per-attempt values with sequence local variables, and do the same job with modeling code where the simulator falls short.

    The variable-latency problem

    A read request carries a tag and the response returns it 1 to 20 cycles later. The check must state that the response tag equals the request tag, whatever the delay.

    A variable declared inside a sequence or property is created fresh for every evaluation attempt, captured at a match item, and carried along that thread until the sequence ends.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet
    property p_tag_match;
      logic [3:0] tag_mem;                 // local: one copy per attempt
      @(posedge clk) disable iff (!rst_n)
        (req, tag_mem = req_tag)           // capture the sampled tag
        |-> ##[1:20] (rsp && rsp_tag == tag_mem);
    endproperty
    ap_tag: assert property (p_tag_match)
      else $error("response tag did not match the requested tag");

    The assignment runs when the left side matches, using the sampled value of the signal. A local is invisible in the action block, so the message above reports the fact, not the captured value.

    Situation

    Use

    Why

    Fixed delay of N cycles

    $past(sig, N)

    Exact and cheap with nothing to carry.

    Variable delay of 1 to 20 cycles

    Local variable

    $past needs a constant cycle offset.

    Several transactions in flight

    Local variable

    Each attempt owns a private copy.

    One copy per thread

    A new attempt starts every clock edge, and a range like ##[1:20] forks one thread per delay value. Every thread carries its own private copy of each local as it existed at the fork point.

    If one forked thread sees the wrong tag, that thread simply fails. The attempt still passes through a sibling thread that matches.

    The bottom two rows carry the answer. Tags keep the two attempts distinct.

    • Slot 0: req is high with tag 5. The first attempt captures 5.

    • Slot 1: req stays high with tag 9. The second attempt captures 9 in its own copy.

    • Slot 2: both attempts wait while req is low.

    • Slot 3: rsp returns tag 9. The second attempt matches and passes.

    • Slot 4: the first attempt still waits while rsp is low.

    • Slot 5: rsp returns tag 5. The first attempt matches and passes.

    Check yourself Two requests overlap and share one module-level tag register. What breaks?

    The second capture overwrites the first before its response arrives. Locals give each attempt a private copy, so overlap is safe.

    Capture at request, compare at response

    Capture at request, compare at response is the single most reused SVA pattern. It checks pipelines, caches and queues: anything where data goes in, transforms predictably, and comes out later.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet
    property p_pipe_data;
      logic [31:0] d;
      @(posedge clk) disable iff (!rst_n)
        (in_valid, d = in_data) |-> ##5 (out_valid && out_data == d + 32'b1);
    endproperty
    ap_pipe: assert property (p_pipe_data);

    The FIFO listing below is not run in this course's checks: Verilator 5.048 does not support the unbounded [*0:$] repetition, and Icarus 13 has no SVA sequences.

    fifo_data_check.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // not-run: Verilator 5.048 does not support unbounded [*0:$]; Icarus 13 has no SVA sequences
    module fifo_data_check #(parameter int DW = 32) (
      input logic          clk, rst_n,
      input logic          push, pop,
      input logic [DW-1:0] din, dout,
      input logic [31:0]   count      // occupancy just before push, from nearby logic
    );
      // At push, n holds the words ahead of ours. Each pop with n != 0 decrements
      // n in its own match item; the exit test reads n one cycle later, never in
      // the same item that writes it. The n != 0 guard matters: without it the pop
      // with n == 0 also continues the repetition, that thread never dies, and a
      // data mismatch on the exit thread could never fail the attempt.
      property p_fifo_data;
        logic [DW-1:0] d;
        logic [31:0]   n;
        @(posedge clk) disable iff (!rst_n)
          (push && !pop, d = din, n = count)
          |-> ((pop && n != 32'b0, n = n - 32'b1) or (!pop)) [*0:$]
              ##1 (pop && n == 32'b0) ##0 (dout == d);
      endproperty
      ap_fifo_data: assert property (p_fifo_data)
        else $error("FIFO data mismatch on pop");
    endmodule

    The illustration assumes push and pop never coincide on the push cycle. A word that never comes out leaves its thread open, so simulation runs add an outer timeout around this shape.

    Common mistakes

    Common pitfall

    A module-level variable assigned in an always block cannot stand in for a local. The second in-flight request overwrites the stored tag before the first response arrives, and the check corrupts itself.

    Interview

    The standard data-integrity question.

    • Say local variable, never $past, for variable latency. $past needs a constant offset.

    • Overlapping requests are safe because each attempt owns a private copy.

    • The FIFO sketch is an occupancy capture plus a per-pop countdown to the matching pop.

    Check yourself Read data must match written data, but the latency varies. What is the expected answer, and what is the instant red flag?

    The expected answer is a sequence local variable. Saying $past is the red flag, since it needs a constant offset.

    Testbench

    On Verilator 5.048, properties with local variables compiled but did not hold the captured value, so their verdicts were wrong. The bench below therefore does the local variable's job in modeling code: an array keeps the data each tag carried and a bit says its response is still owed, and a one-step assertion checks every response against them. On a simulator that runs local variables, the property form above is shorter; this form runs everywhere.

    var_latency_pipe.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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // Two lanes with different latency; even tags take lane 0 (fast), odd tags lane 1 (slow).
    module var_latency_pipe (
      input  logic       clk, rst_n, bug,
      input  logic       req,
      input  logic [3:0] req_tag,
      input  logic [7:0] req_data,
      output logic       rsp,
      output logic [3:0] rsp_tag,
      output logic [7:0] rsp_data,
      output logic       busy0, busy1
    );
      logic lane0_valid, lane1_valid;
      logic [3:0] lane0_tag, lane1_tag;
      logic [7:0] lane0_data, lane1_data;
      logic [1:0] lane0_cnt, lane1_cnt;
      assign busy0 = lane0_valid;
      assign busy1 = lane1_valid;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          lane0_valid <= 0; lane1_valid <= 0; lane0_cnt <= 0; lane1_cnt <= 0;
          rsp <= 0; rsp_tag <= 0; rsp_data <= 0;
        end else begin
          rsp <= 0;
          if (lane0_valid && lane0_cnt == 0) begin
            rsp <= 1; rsp_tag <= lane0_tag; rsp_data <= lane0_data ^ (bug ? 8'h10 : 8'h00); lane0_valid <= 0;
          end else if (lane1_valid && lane1_cnt == 0) begin
            rsp <= 1; rsp_tag <= lane1_tag; rsp_data <= lane1_data; lane1_valid <= 0;
          end
          if (lane0_valid && lane0_cnt != 0) lane0_cnt <= lane0_cnt - 1;
          if (lane1_valid && lane1_cnt != 0) lane1_cnt <= lane1_cnt - 1;
          if (req && !req_tag[0] && !lane0_valid) begin
            lane0_valid <= 1; lane0_tag <= req_tag; lane0_data <= req_data; lane0_cnt <= 1;
          end
          if (req && req_tag[0] && !lane1_valid) begin
            lane1_valid <= 1; lane1_tag <= req_tag; lane1_data <= req_data; lane1_cnt <= 3;
          end
        end
    endmodule
    
    Terminal
    6 tagged requests over two lanes: 6 responses checked, 0 failures
    one corrupted response: 1 failure
    PASS: every response matched its own tag's data across variable latency, and the corrupted one was reported

    Remember

    • Variable latency needs a local variable, never $past.

    • Each attempt owns its own copy, so overlapping requests are safe.

    • Modeling code keyed by tag is the portable equivalent.

    Rule of thumb: anything that varies per transaction is captured per transaction.

    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 compiled local variables but did not hold the captured value; the bench keys the expected data by tag in modeling code instead.

    Related topics

    • Sampled value functions

    Previous

    124 / 259

    Next: Multi-Clock Assertions

    Up next

    Up nextMulti-Clock Assertions

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • expect, Procedural Concurrent Assertions
    • Checkers and Assertion Control
    clkreqtag59rsprtag095
    Two overlapping tagged requests answered out of order