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
    • SystemVerilog Data Types
    • Arrays: Packed, Dynamic, Queues & Associative
      • Packed vs Unpacked Arrays
      • Dynamic Arrays and Queues
      • Associative Arrays
      • Array Methods and Selection
    • Procedural Code & Scheduling
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
    • Interfaces, Modports & Clocking Blocks
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Arrays: Packed, Dynamic, Queues & Associative
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. Arrays: Packed, Dynamic, Queues & Associative
  5. Dynamic Arrays and Queues

Part 1 · Language Foundations · Intermediate

Dynamic Arrays and Queues

Arrays sized at runtime and queues that grow per push, with an in-order checker.

On this page

    Why this matters

    A check popped from an empty queue, got 0, compared it with a DUT output of 0 and passed. Queues are the backbone of testbench checking, and their empty and full cases are where tests lie.

    After this lesson you can: size a dynamic array at run time and grow it safely, use a queue as a FIFO, and build an in-order checker with a residual check.

    Size decided at runtime

    A dynamic array is declared with empty brackets, such as int data[]. It holds no storage until new[n] allocates it, and its size is 0 before that.

    Indexing before allocation is out of bounds. Guard every use with a size check instead of relying on any simulator diagnostic, which is tool-dependent and not guaranteed by the standard.

    Calling new[n] again discards old contents. To grow while preserving them, pass the old array along: data = new[data.size()*2](data).

    The preserving form copies every old element, so it costs O(n) in practice. Growing by one element per loop iteration is quadratic, and that workload belongs in a queue.

    Operation

    Meaning

    size()

    Current element count, 0 before allocation and after delete().

    new[n]

    Allocate n default-initialized elements, discarding previous contents.

    new[n](src)

    Allocate n elements, copying the first min(n, src.size()) from src.

    b = a

    Value copy: b gets independent storage with a copy of every element.

    delete()

    Release all storage; the variable stays usable for a later new[].

    Array assignment copies values, so later writes to one array never affect the other. (Class handles, in the OOP part, behave differently.)

    Predict first data holds 0, 10, 20, 30. After data = new[8](data), what are data[3] and data[7]?

    30 and 0. The first four elements are copied from the old array, and the four new ones get the default value for int.

    Queues that grow per push

    A queue is declared with [$], such as int q[$]. It starts empty with no allocation step and grows one element per push.

    Adding or removing at either end is O(1) in practice, while insertion in the middle costs O(n). The index $ names the last element, so q[0] is the front and q[$] is the back.

    Work at the ends is cheap; work in the middle moves everything after it.

    Slices such as q[1:$] produce a new queue value. Combined with concatenation they perform list surgery without explicit loops.

    A bounded queue such as int q[$:7] holds at most 8 elements. After any write, elements past the bound are discarded and the LRM requires a warning: push_back on a full queue loses the new item, while push_front loses the back item. Size the bound from the protocol and never rely on overflow pushes in a passing test.

    The expected-queue checker

    The key queue idiom is the expected queue. The reference model, code that predicts what the DUT should output, pushes each prediction with push_back. The code that watches the DUT's output pops the oldest prediction with pop_front and compares. FIFO order gives in-order checking for free.

    A non-empty queue at end of test means the DUT dropped transactions. Checking residual size() is part of the pattern, not an afterthought.

    Common mistakes

    Common pitfall

    pop_front() on an empty queue returns the default value plus a tool-dependent diagnostic. Guard with a size() check first, or missing predictions look like real data.

    Interview

    Likely question: “how do you check in-order data through a DUT?”

    • Predict into a queue with push_back from the reference model.

    • Compare each DUT output against pop_front in arrival order.

    • Drain-check residual size() at end of test for dropped items.

    Check yourself data holds 4 elements and the code says data = new[8], without (data). What survives?

    Nothing. Bare new[8] allocates eight fresh default elements and discards the old contents.

    Check yourself Why is per-iteration data = new[data.size()+1](data) a queue-shaped problem?

    Each append copies all n elements, so the loop is quadratic in practice. A queue appends in O(1) per push.

    Check yourself A bounded queue int q[$:3] already holds 4 elements and the test pushes a fifth with push_back. What happens?

    The new item is dropped and the queue keeps its 4 elements; the bench checks exactly this. A push_front would instead keep the new item and drop the back one. Either way the queue never grows past its bound.

    Testbench

    Runtime sizing, preserving resize, value copies, FIFO order, slicing and a bounded queue in one self-checking testbench.

    Sizing an array from a command-line value, so one compiled testbench covers many depths, is in Plusargs and configuration.

    design.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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    module tb_dyn_queue;
      int data[];
      int snapshot[];
      int q[$];
      int bq[$:3];      // bounded: at most 4 elements
      int exp_q[$];     // predictions, oldest first
      int got;
      int want;
      int errors = 0;
    
      initial begin
        data = new[4];
        foreach (data[i]) data[i] = i * 10;   // 0, 10, 20, 30
        if (data.size() != 4) errors++;
    
        data = new[8](data);                  // grow, old 4 preserved
        if (data.size() != 8) errors++;
        if (data[3] !== 30 || data[7] !== 0) errors++;
    
        data = new[2](data);                  // shrink keeps first 2 only
        if (data.size() != 2 || data[1] !== 10) errors++;
    
        snapshot = new[data.size()](data);    // explicit deep copy
        snapshot[0] = -1;
        if (data[0] !== 0) errors++;          // value copy: independent storage
        data.delete();
        if (data.size() != 0 || snapshot.size() != 2) errors++;
    
        q.push_back(10);
        q.push_back(20);
        q.push_front(5);                      // {5, 10, 20}
        if (q[0] !== 5 || q[$] !== 20 || q.size() != 3) errors++;
    
        exp_q.push_back(5);
        exp_q.push_back(10);
        exp_q.push_back(20);
        while (q.size() > 0) begin            // FIFO drain against predictions
          got = q.pop_front();
          want = exp_q.pop_front();
          if (got !== want) errors++;
        end
        if (exp_q.size() != 0) errors++;      // residual check: nothing dropped
    
        q = {1, 2, 3, 4, 5};
        q = q[1:$];                           // drop front -> {2,3,4,5}
        q = q[0:$-1];                         // drop back  -> {2,3,4}
        if (q.size() != 3 || q[0] !== 2 || q[$] !== 4) errors++;
        q.delete();
        if (q.size() != 0) errors++;
    
        bq.push_back(1);
        bq.push_back(2);
        bq.push_back(3);
        bq.push_back(4);
        if (bq.size() != 4 || bq[$] !== 4) errors++;
        bq.push_back(5);                      // full: the new item is dropped
        if (bq.size() != 4 || bq[$] !== 4) errors++;
    
        if (errors != 0) $fatal(1, "FAIL: dynamic array and queue self-check");
        $display("PASS: resize preserves contents, copies stay independent, FIFO drain matches predictions, bound holds");
        $finish;
      end
    endmodule
    Terminal
    PASS: resize preserves contents, copies stay independent, FIFO drain matches predictions, bound holds

    Project step

    Build the PacketFIFO reference model: an expected queue of 9-bit {last, byte} entries. Push every byte the FIFO accepts, pop and compare every byte it delivers, and finish with the residual check. Use the Part 0 packet: header 22, payload A5 3C, check byte BB with last set.

    One way to write it, with a four-entry stand-in for the FIFO (the packages topic builds the real one):

    pkt_fifo_v0.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
    // PacketFIFO v0 stand-in: four entries of {last, byte}. The packages topic builds the real one.
    module pkt_fifo_v0 (
      input  logic       clk, rst_n,
      input  logic       in_valid,
      input  logic [8:0] in_data,
      output logic       in_ready,
      output logic       out_valid,
      output logic [8:0] out_data,
      input  logic       out_ready
    );
      logic [8:0] mem [0:3];
      logic [2:0] wr, rd;                   // one extra bit tells full from empty
      assign in_ready  = (wr - rd) != 3'd4;
      assign out_valid = wr != rd;
      assign out_data  = mem[rd[1:0]];
      always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          wr <= 0;
          rd <= 0;
        end else begin
          if (in_valid && in_ready) begin
            mem[wr[1:0]] <= in_data;
            wr <= wr + 1;
          end
          if (out_valid && out_ready) rd <= rd + 1;
        end
    endmodule
    
    Terminal
    read 022  expected 022
    read 0a5  expected 0a5
    read 03c  expected 03c
    read 1bb  expected 1bb
    PASS: 4 bytes out in order, expected queue empty

    Remember

    • new[n] discards old contents; newn keeps them.

    • Queues are cheap at both ends and costly in the middle; q[$] is the back.

    • An expected queue checks order for free; a non-empty queue at the end means lost data.

    Rule of thumb: check size() before every pop, and check that the queue is empty at the end.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 cannot run the dynamic-array and queue bench (no queue slices); the PacketFIFO bench runs on both.

    The bounded-queue overflow behaves the same on both simulators; Icarus 13 prints a warning for it and Verilator 5.048 does not, so the bench checks the size instead of the message.

    The PacketFIFO bench was also run with a planted bug (the read pointer stepping by 2); it failed with 2 mismatches, as it should.

    Related topics

    • Next: Associative Arrays

    Previous

    13 / 259

    Next: Associative Arrays

    Up next

    Up nextAssociative Arrays

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Array Methods and Selection