Skip to content

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

SystemVerilog — Dynamic Array Syntax
// ── 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
OperationSyntaxResultNotes
Declaretype name []Size = 0, unallocatedNo memory reserved
Allocatename = new[N]N elements, default-initPrevious data discarded
Allocate + initname = new[N](src)N elements, copied from srcsrc can be same array (resize)
Get size$size(name) or name.size()int — current element countReturns 0 if deleted/unallocated
Deletename.delete()Size = 0, memory freedEquivalent to new[0]
Copydst = srcDeep copy of all elementsIndependent storage after copy

Visual — Lifecycle and Memory Layout

Allocation Lifecycle

StepCodeArray state$size()Access result
1. Declaredint arr []Null / empty0Fatal error
2. Allocatedarr = new[4][0, 0, 0, 0]4Returns element
3. Writtenforeach fill[10, 20, 30, 40]4Returns written value
4. Grownarr = new[6](arr)[10, 20, 30, 40, 0, 0]6Old data preserved at [0..3]
5. Shrunkarr = new[2](arr)[10, 20]2[2..5] gone permanently
6. Deletedarr.delete()Null / empty0Fatal error

Deep Copy vs Class Handle Copy

ScenarioCodeWhat happensAre they independent?
Standalone dynamic array copyb = a (both are int arr[])Full deep copy — each element of a copied into new storage for bYes — 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 fieldNo — obj2.arr[0] IS obj1.arr[0]
Proper class deep copyobj2 = new obj1 or custom copy()Creates new object, copies all fields including re-allocating the dynamic arrayYes — 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)10203040
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

Example 1 — Dynamic Array Basics
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
 
endmodule

Expected output:

Simulation 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 = 0

Example 2 — Intermediate: Variable-Length AXI Burst Driver

Example 2 — AXI Burst with Dynamic Data Array
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
endmodule

Example 3 — Verification: Dynamic Scoreboard Event Log

Example 3 — Growing Event Log Using Dynamic Array
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
 
endmodule

Expected output:

Simulation 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: 3

Example 4 — Corner Case: Class Handle vs Array Deep Copy Trap

Example 4 — Shallow vs 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
 
endmodule

Expected output:

Simulation 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]=999

Simulation 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.

EventDynamic array behaviorStatic array behavior
Access before allocationFatal error — simulation stopsReturns X (warning only)
Access after delete()Fatal errorN/A — no delete on static
Out-of-bounds readFatal errorReturns X (warning)
Out-of-bounds writeFatal errorSilently ignored
Uninitialized elements (logic)X (same as static)X
Uninitialized elements (int)0 (same as static)0

Where Dynamic Arrays Belong in Real Verification

Verification Patterns Using Dynamic Arrays
// ── 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;
endfunction

Bugs Engineers Hit With Dynamic Arrays

Bug 1 — Access Before Allocation: Fatal Crash

Bug 1 — Unallocated Array Access
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

Bug 2 — Growing Without the Copy Argument
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 — correct

Bug 3 — Class Handle Aliasing: Modifying the "Copy" Corrupts the Original

Bug 3 — Shallow Copy Aliasing in Scoreboard
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 — correct

Bug 4 — Constraining data.size() Without Understanding Solver Allocation

Bug 4 — Dynamic Array Size Constraint Pitfall
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
endclass

Interview 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.
TaskCorrect approachCommon mistake
Grow array, keep dataarr = new[N](arr)arr = new[N] — destroys existing data
Store transaction in scoreboardClone the object, re-allocate its array fieldStore the handle directly — aliasing bug
Check if allocatedif ($size(arr) > 0)No check — fatal crash on empty array
Constrained random with variable sizeconstraint { data.size() == len; }No size constraint — solver picks arbitrary size
Iterate all elementsforeach (arr[i]) — safe on empty arrayfor (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 with delete(). 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() == len lets 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.