Dynamic Arrays
Runtime sizing with new[], delete(), resize, copy semantics, constraints.
Module 3 · Page 3.2
When the Size Is Known Only at Runtime
Static arrays are great until you need to model an AXI burst that can be 1 to 256 beats long, or collect an unknown number of error events during a test run, or build a scoreboard that tracks exactly as many transactions as the stimulus generates. At that point you either pre-allocate the maximum size and waste memory, or you reach for a dynamic array.
Dynamic arrays in SystemVerilog behave like unpacked static arrays in almost every way — you index them with [], iterate with foreach, copy them with =, compare with == — but the size is set at runtime with new[N] and can be changed at any point. Before you call new[], the array is empty and any access produces a fatal error. After delete(), it is empty again.
The critical behavior that trips experienced engineers: when you assign one dynamic array to another with b = a, you get a deep copy — both arrays have independent storage. That sounds safe, but inside class objects the story changes. A class handle holding a dynamic array, when the class handle itself is copied with =, creates a shallow copy of the handle — both variables point to the same underlying data. Modify one and you silently corrupt the other.
How Dynamic Arrays Actually Work
A dynamic array starts as a null reference — no memory is allocated until you call new[N]. That call allocates N elements, each initialized to the default value of the element type (0 for int/bit, X for logic). You can then call new[M] again to resize — if M is larger than the current size, new elements are default-initialized; if smaller, the excess elements are discarded.
The resize operation also supports copying: arr.new[M](arr) allocates M elements and copies as many existing elements as possible — the standard pattern for growing a dynamic array while preserving its contents.
- new[N] — Allocate — Creates N elements, all default-initialized. Replaces any previous allocation. The element type determines the initial value (X for logic, 0 for int/bit).
- delete() — Free — Releases all memory. Array size becomes 0. Any subsequent access without a new new[] call is a fatal error. Equivalent to new[0].
- newM — Resize + Copy — Allocates M elements and copies from src. Elements beyond src's size are default-initialized. The most common way to grow an array without losing existing data.
- $size() — Current Count — Returns the current number of allocated elements. Returns 0 for an unallocated or deleted array. Use this instead of hardcoded sizes to make your code size-independent.
Syntax — Every Operation You'll Use
// ── Declaration (no allocation yet — size = 0) ───────────────────
logic [7:0] bytes []; // unallocated byte array
int scores []; // unallocated int array
string names []; // unallocated string array
// ── Allocation ────────────────────────────────────────────────────
bytes = new[16]; // allocate 16 elements (logic → X-initialized)
scores = new[8]; // allocate 8 elements (int → 0-initialized)
bytes = new[8]('{default:8'hFF}); // allocate + initialize via literal
// ── Resize — keep existing data ───────────────────────────────────
bytes = new[32](bytes); // grow from 16 to 32; old data preserved [0..15]
bytes = new[8](bytes); // shrink to 8; elements [8..15] dropped
// ── Element access — identical to static array ────────────────────
bytes[0] = 8'hAB;
logic [7:0] val = bytes[3];
// ── Size query ────────────────────────────────────────────────────
$size(bytes) // current number of elements
bytes.size() // equivalent method call form
// ── Iteration ─────────────────────────────────────────────────────
foreach (bytes[i])
bytes[i] = i;
// ── Whole-array copy (deep copy) ──────────────────────────────────
logic [7:0] copy [];
copy = bytes; // deep copy — independent storage
copy[0] = 8'hFF; // does NOT affect bytes[0]
// ── Whole-array comparison ────────────────────────────────────────
if (bytes == copy) … // element-wise equal (never X even if elements are X)
if (bytes !== copy) … // case inequality — catches X differences too
// ── Delete ────────────────────────────────────────────────────────
bytes.delete(); // free memory, size becomes 0
// bytes.size() == 0 after delete| Operation | Syntax | Result | Notes |
|---|---|---|---|
| Declare | type name [] | Size = 0, unallocated | No memory reserved |
| Allocate | name = new[N] | N elements, default-init | Previous data discarded |
| Allocate + init | name = new[N](src) | N elements, copied from src | src can be same array (resize) |
| Get size | $size(name) or name.size() | int — current element count | Returns 0 if deleted/unallocated |
| Delete | name.delete() | Size = 0, memory freed | Equivalent to new[0] |
| Copy | dst = src | Deep copy of all elements | Independent storage after copy |
Visual — Lifecycle and Memory Layout
Allocation Lifecycle
| Step | Code | Array state | $size() | Access result |
|---|---|---|---|---|
| 1. Declared | int arr [] | Null / empty | 0 | Fatal error |
| 2. Allocated | arr = new[4] | [0, 0, 0, 0] | 4 | Returns element |
| 3. Written | foreach fill | [10, 20, 30, 40] | 4 | Returns written value |
| 4. Grown | arr = new[6](arr) | [10, 20, 30, 40, 0, 0] | 6 | Old data preserved at [0..3] |
| 5. Shrunk | arr = new[2](arr) | [10, 20] | 2 | [2..5] gone permanently |
| 6. Deleted | arr.delete() | Null / empty | 0 | Fatal error |
Deep Copy vs Class Handle Copy
| Scenario | Code | What happens | Are they independent? |
|---|---|---|---|
| Standalone dynamic array copy | b = a (both are int arr[]) | Full deep copy — each element of a copied into new storage for b | Yes — modifying b[0] does NOT change a[0] |
| Class handle copy (contains dynamic array) | obj2 = obj1 (class handles) | Shallow copy — both handles point to the same object and its array field | No — obj2.arr[0] IS obj1.arr[0] |
| Proper class deep copy | obj2 = new obj1 or custom copy() | Creates new object, copies all fields including re-allocating the dynamic array | Yes — independent after explicit deep copy |
Resize With Data Preservation
arr = new[6](arr) — growing from 4 to 6 elements:
| Index | [0] | [1] | [2] | [3] | [4] | [5] |
|---|---|---|---|---|---|---|
| Before (size 4) | 10 | 20 | 30 | 40 | — | — |
| After (size 6) | 10 ✓ | 20 ✓ | 30 ✓ | 40 ✓ | 0 (new) | 0 (new) |
Green = preserved from original. Orange = new elements, default-initialized to 0 (int type).
Code Examples — Runtime Sizing to Verification Patterns
Example 1 — Beginner: Allocate, Fill, Resize, Delete
module tb_dyn_basics;
int arr [];
initial begin
// ── Allocate ──────────────────────────────────────────────────
arr = new[4];
$display("After new[4]: size = %0d", $size(arr)); // 4
// ── Fill ──────────────────────────────────────────────────────
foreach (arr[i]) arr[i] = (i + 1) * 10;
$display("Filled: %p", arr); // '{10, 20, 30, 40}
// ── Grow and preserve ─────────────────────────────────────────
arr = new[6](arr);
$display("After grow to 6: %p", arr); // '{10, 20, 30, 40, 0, 0}
// ── Shrink ────────────────────────────────────────────────────
arr = new[2](arr);
$display("After shrink to 2: %p", arr); // '{10, 20}
// ── Deep copy: b = a → independent ─────────────────────────
int copy [];
copy = arr;
copy[0] = 999;
$display("arr[0]=%0d copy[0]=%0d", arr[0], copy[0]); // 10 999 — independent
// ── Delete ────────────────────────────────────────────────────
arr.delete();
$display("After delete: size = %0d", $size(arr)); // 0
$finish;
end
endmoduleExpected output:
After new[4]: size = 4
Filled: '{10, 20, 30, 40}
After grow to 6: '{10, 20, 30, 40, 0, 0}
After shrink to 2: '{10, 20}
arr[0]=10 copy[0]=999
After delete: size = 0Example 2 — Intermediate: Variable-Length AXI Burst Driver
class axi_write_txn;
rand logic [31:0] addr;
rand int unsigned burst_len; // 1 to 16 beats
rand logic [31:0] data []; // size unknown until constrained
constraint len_range { burst_len inside {[1:16]}; }
constraint data_size { data.size() == burst_len; } // dynamic size constraint
constraint addr_align { addr[1:0] == 2'b00; }
// Solver calls new[] automatically when a dynamic array size is constrained
function void print();
$display("AXI WRITE addr=0x%08h len=%0d", addr, burst_len);
foreach (data[i])
$display(" beat[%0d] = 0x%08h", i, data[i]);
endfunction
endclass
module tb_axi_dyn;
initial begin
axi_write_txn txn = new();
repeat (3) begin
if (!txn.randomize())
$fatal(1, "Randomize failed");
txn.print();
end
$finish;
end
endmoduleExample 3 — Verification: Dynamic Scoreboard Event Log
module tb_event_log;
typedef struct {
int timestamp;
logic [7:0] opcode;
logic [31:0] addr;
} event_t;
event_t log []; // starts empty
int log_count = 0;
// Append one event — resizes the array by +1 each call
task automatic log_event(input int ts, input logic [7:0] op, input logic [31:0] a);
log = new[log_count + 1](log); // grow by 1, preserve existing entries
log[log_count] = '{ts, op, a};
log_count++;
endtask
// Print all logged events
task automatic dump_log();
$display("=== Event Log (%0d entries) ===", log_count);
foreach (log[i])
$display(" [%0d] t=%0d op=0x%02h addr=0x%08h",
i, log[i].timestamp, log[i].opcode, log[i].addr);
endtask
initial begin
log_event(100, 8'h10, 32'h0000_1000);
log_event(200, 8'h20, 32'h0000_2000);
log_event(300, 8'hFF, 32'hFFFF_FFFF);
dump_log();
$display("Final log size: %0d", $size(log));
$finish;
end
endmoduleExpected output:
=== Event Log (3 entries) ===
[0] t=100 op=0x10 addr=0x00001000
[1] t=200 op=0x20 addr=0x00002000
[2] t=300 op=0xFF addr=0xFFFFFFFF
Final log size: 3Example 4 — Corner Case: Class Handle vs Array Deep Copy Trap
class packet;
int payload [];
function new(int sz); payload = new[sz]; endfunction
endclass
module tb_copy_trap;
initial begin
// ── Case 1: Standalone dynamic arrays — DEEP copy ─────────────
int a [] = new[3]('{1, 2, 3});
int b [];
b = a; // deep copy of array data
b[0] = 99;
$display("Standalone: a[0]=%0d b[0]=%0d", a[0], b[0]); // 1 99 — independent ✓
// ── Case 2: Class handle copy — SHALLOW (DANGEROUS!) ─────────
packet p1 = new(3);
p1.payload = '{10, 20, 30};
packet p2;
p2 = p1; // SHALLOW: p2 points to same object as p1
p2.payload[0] = 99;
$display("Class handle: p1.payload[0]=%0d p2.payload[0]=%0d",
p1.payload[0], p2.payload[0]); // 99 99 — SAME! Bug!
// ── FIX: use new to create an independent copy ────────────────
packet p3 = new p1; // copy constructor — shallow copy of fields
// But p3.payload still shares storage with p1.payload!
// True deep copy requires explicitly re-allocating the array:
p3.payload = new[p1.payload.size()](p1.payload);
p3.payload[0] = 999;
$display("Deep copy: p1.payload[0]=%0d p3.payload[0]=%0d",
p1.payload[0], p3.payload[0]); // 99 999 — independent ✓
$finish;
end
endmoduleExpected output:
Standalone: a[0]=1 b[0]=99
Class handle: p1.payload[0]=99 p2.payload[0]=99
Deep copy: p1.payload[0]=99 p3.payload[0]=999Simulation Behavior — What Really Happens
Access Before Allocation: Fatal, Not X
Accessing an unallocated or deleted dynamic array is a fatal simulation error, not a silent X return. This is the key difference from static arrays: static arrays return X on out-of-bounds; dynamic arrays crash the simulation entirely. The simulator throws something like "Fatal: index out of range for dynamic array of size 0". The practical implication: always check $size(arr) > 0 before accessing, especially in reusable task/function code that might be called before the test has allocated the array.
Constraint Solver and Dynamic Array Sizing
When you constrain data.size() inside a class, the constraint solver automatically allocates the array to the solved size before randomizing the elements. You do not need to call new[] manually before randomize(). However, if you access data before the first randomize() call — say, in the class constructor or a pre-randomize callback — the array is still unallocated and any access is fatal.
| Event | Dynamic array behavior | Static array behavior |
|---|---|---|
| Access before allocation | Fatal error — simulation stops | Returns X (warning only) |
| Access after delete() | Fatal error | N/A — no delete on static |
| Out-of-bounds read | Fatal error | Returns X (warning) |
| Out-of-bounds write | Fatal error | Silently ignored |
| Uninitialized elements (logic) | X (same as static) | X |
| Uninitialized elements (int) | 0 (same as static) | 0 |
Where Dynamic Arrays Belong in Real Verification
// ── 1. TRANSACTION CLASS: variable-length burst payload ───────────
class axi_burst;
rand int unsigned len;
rand logic [31:0] data [];
constraint c { len inside {[1:256]}; data.size() == len; }
endclass
// ── 2. SCOREBOARD: collect all received transactions, then compare ─
logic [31:0] rcv_data []; // grows as beats arrive
int rcv_count = 0;
// In monitor task — called per beat:
// rcv_data = new[rcv_count+1](rcv_data);
// rcv_data[rcv_count++] = captured_beat;
// End of burst — compare against expected:
// if (rcv_data !== exp_data) $error("burst mismatch");
// ── 3. CONSTRAINT: size-dependent payload pattern ─────────────────
class pkt_with_crc;
rand int unsigned payload_len;
rand logic [7:0] payload [];
rand logic [15:0] crc;
constraint len_c { payload_len inside {[4:64]}; }
constraint size_c { payload.size() == payload_len; }
// post-randomize: compute real CRC from payload
function void post_randomize();
crc = 16'h0;
foreach (payload[i]) crc ^= {8'h0, payload[i]};
endfunction
endclass
// ── 4. UVM sequence item: standard pattern ────────────────────────
// class my_seq_item extends uvm_sequence_item;
// rand logic [7:0] data [];
// rand int unsigned len;
// constraint data_sz { data.size() == len; }
// endclass
// ── 5. COVERAGE: collect unique values seen ───────────────────────
int seen_values []; // grows as new values are observed
function void record_value(int v);
foreach (seen_values[i])
if (seen_values[i] == v) return; // already recorded
seen_values = new[seen_values.size()+1](seen_values);
seen_values[seen_values.size()-1] = v;
endfunctionBugs Engineers Hit With Dynamic Arrays
Bug 1 — Access Before Allocation: Fatal Crash
int buf []; // declared but not allocated — size = 0
// BUGGY: accessing index 0 on a size-0 array
buf[0] = 42; // FATAL: "dynamic array index out of range" — simulation crashes
// Also fatal:
foreach (buf[i]) $display(buf[i]); // safe only if size>0; empty array = no iterations
// foreach is safe — it just loops 0 times on a size-0 array
// But buf[0] directly when size=0 is always fatal
// FIXED: always allocate before accessing by index
buf = new[4]; // allocate first
buf[0] = 42; // now safe
// DEFENSIVE: check before access
if ($size(buf) > 0) buf[0] = 42;Bug 2 — Resize Without Preserving: Data Silently Lost
int log [] = new[4]('{10, 20, 30, 40});
// BUGGY: new[N] without the copy argument discards existing data
log = new[8]; // ALL original data gone — log is now '{0,0,0,0,0,0,0,0}
$display("log[0] = %0d", log[0]); // 0 — expected 10!
// CORRECT: pass the array itself as the copy source
log = new[8](log); // grows to 8, preserves [10,20,30,40], zeros [4..7]
$display("log[0] = %0d", log[0]); // 10 — correctBug 3 — Class Handle Aliasing: Modifying the "Copy" Corrupts the Original
class txn;
logic [31:0] data [];
endclass
txn sent_txn = new();
sent_txn.data = new[4]('{1,2,3,4});
// BUGGY: scoreboard saves a "copy" to compare later
txn saved = sent_txn; // SHALLOW COPY — both point to same object!
// Driver modifies the transaction for the next beat
sent_txn.data[0] = 99; // also silently modifies saved.data[0]!
$display("saved.data[0] = %0d", saved.data[0]); // 99 — WRONG, expected 1
// FIXED: allocate new object + copy array explicitly
txn saved_fixed = new();
saved_fixed.data = new[sent_txn.data.size()](sent_txn.data);
sent_txn.data[0] = 99;
$display("saved_fixed.data[0] = %0d", saved_fixed.data[0]); // 1 — correctBug 4 — Constraining data.size() Without Understanding Solver Allocation
class bad_txn;
rand logic [7:0] data [];
rand int unsigned len;
// BUGGY: no constraint linking len to data.size()
// Solver picks random len and random data size independently
// len=5 but data might have size=3 — they are unrelated!
constraint len_c { len inside {[1:16]}; }
// Missing: constraint size_c { data.size() == len; }
endclass
// CORRECT: link them explicitly
class good_txn;
rand logic [7:0] data [];
rand int unsigned len;
constraint len_c { len inside {[1:16]}; }
constraint size_c { data.size() == len; } // solver now allocates correctly
endclassInterview Questions
Beginner Level
Q1: How do you allocate a dynamic array of 10 integers and initialize them all to 0?int arr [] = new[10]; — since int is a two-state type, new[N] initializes all elements to 0 automatically. If you need to be explicit or initialize to a non-zero value: arr = new[10]('{default: 0}). For logic-type arrays you must initialize explicitly, because they default to X. Q2: What is the difference between arr = new[8] and arr = new8?new[8] allocates 8 elements and discards any existing data — all elements are default-initialized. new8 allocates 8 elements and copies from the existing arr — elements within the old size are preserved, any new elements are default-initialized. Use the second form whenever you need to resize without losing existing data.
Intermediate Level
Q3: What happens when you access index 5 on a dynamic array with 3 elements? It is a fatal simulation error — the simulator terminates with an out-of-range error. This is fundamentally different from static arrays, which silently return X. Dynamic arrays enforce strict bounds checking at runtime. This makes them safer in one sense (bugs crash loudly) but requires defensive programming: always verify $size(arr) > target_index before accessing when the size is variable. Q4: Does assigning a dynamic array to another (b = a) create a shared reference or a copy? For standalone dynamic array variables, assignment creates a deep copy — independent storage. Modifying b after assignment does not affect a. However, if the dynamic array is a field inside a class object, copying the class handle with = creates a shallow copy of the handle — both handles point to the same object. Modifying the field through either handle modifies the shared object.
Experienced Engineer Level
Q5: In a UVM scoreboard that collects transactions into a dynamic array, you notice that after storing a transaction the scoreboard's copy changes whenever the driver modifies the original object. What is the root cause and how do you fix it? The root cause is shallow copy of the class handle. The scoreboard stores the handle reference, not the object data. When the driver modifies the original transaction object (to prepare the next beat), the scoreboard's stored handle still points to that same object. Fix: implement a clone() or copy() function in the transaction class that creates a new object and explicitly re-allocates and copies the dynamic array: cloned.data = newthis.data.size(). The scoreboard must call clone() before storing the transaction. In UVM, the standard pattern is to call $cast(stored_txn, received_txn.clone()) in the write() task.
Best Practices & Coding Guidelines
- Always allocate before access — Dynamic arrays start at size 0. Any indexed access before new[] is fatal. Add a size guard or allocate in the constructor. Never assume allocation happened elsewhere.
- Use newN for resize — When growing or shrinking, always pass the existing array as the copy source unless you explicitly want to discard all data. new[N] alone is a destructive operation.
- Implement clone() in transaction classes — Any class with a dynamic array field needs an explicit deep copy method. Never rely on obj2 = obj1 for a scoreboard — it shares the handle, not the data.
- Never use in synthesizable RTL — Dynamic arrays require runtime memory allocation which has no hardware equivalent. Keep them strictly in testbench, UVM components, and simulation-only utility classes.
| Task | Correct approach | Common mistake |
|---|---|---|
| Grow array, keep data | arr = new[N](arr) | arr = new[N] — destroys existing data |
| Store transaction in scoreboard | Clone the object, re-allocate its array field | Store the handle directly — aliasing bug |
| Check if allocated | if ($size(arr) > 0) | No check — fatal crash on empty array |
| Constrained random with variable size | constraint { data.size() == len; } | No size constraint — solver picks arbitrary size |
| Iterate all elements | foreach (arr[i]) — safe on empty array | for (int i=0; i<N; i++) with hardcoded N |
Summary
Dynamic arrays solve the "I don't know the size at compile time" problem cleanly. The new[N] / delete() lifecycle is simple; the element access syntax is identical to static arrays; the constraint solver handles size constraints natively. The two things that produce real bugs: forgetting to allocate before access (which crashes, loudly), and the class-handle shallow-copy trap (which corrupts silently).
- Allocate with
new[N], release withdelete(). Any access on an unallocated array is a fatal error. - Resize with
new[M](arr)to preserve existing data.new[M]alone discards everything. - Standalone array assignment is a deep copy. Class handle assignment is shallow. Know which one you have.
- Constraint
data.size() == lenlets the solver control the size. Without it, the array size and your length variable are unrelated. - Verification-only. No synthesis tool supports dynamic arrays — they belong exclusively in the testbench.