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 |
|---|---|
| Current element count, 0 before allocation and after |
| Allocate n default-initialized elements, discarding previous contents. |
| Allocate n elements, copying the first min(n, src.size()) from src. |
| Value copy: b gets independent storage with a copy of every element. |
| Release all storage; the variable stays usable for a later |
Array assignment copies values, so later writes to one array never affect the other. (Class handles, in the OOP part, behave differently.)
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.
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.
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 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.
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.
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.
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.
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.
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
endmodulePASS: resize preserves contents, copies stay independent, FIFO drain matches predictions, bound holdsOne way to write it, with a four-entry stand-in for the FIFO (the packages topic builds the real one):
// 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
read 022 expected 022
read 0a5 expected 0a5
read 03c expected 03c
read 1bb expected 1bb
PASS: 4 bytes out in order, expected queue emptyRun 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.