Dynamic Arrays, Assignment & Arguments
How to declare and use dynamic arrays in SystemVerilog — allocating with new[], growing with value-preservation, the three built-in methods, complete array assignment compatibility rules, and how to pass any array kind as a task or function argument.
🔧 What is a Dynamic Array?
A dynamic array is an unpacked array whose size is not known at compile time. Instead of declaring a fixed number of elements, you declare an empty placeholder and allocate the exact number of elements you need at runtime using new[]. You can resize it at any point, and the storage is automatically managed.
Fixed-size array — size at compile time
// Size hardcoded — cannot change int fixed[8]; // always 8 elements int fixed2[0:255]; // always 256 elements // If your packet length varies, you must allocate // the maximum size and waste memory: byte payload[1500]; // always 1500 bytes, even for 1-byte packet!
Dynamic array — size at runtime
// Empty until new[] is called int dyn[]; // no elements yet byte payload[]; // no elements yet // Allocate exactly what you need: payload = new[len]; // len bytes, chosen at runtime payload = new[1]; // 1 byte next time payload = new[1500];// 1500 bytes another time
new[]. Before that first new[], size() returns 0 and any element access returns the type’s default value.
📄 Declaring Dynamic Arrays
The syntax is the same as a fixed-size unpacked array, but you use empty brackets [] for the dynamic dimension. Any number of packed dimensions can precede the variable name as usual.
// Syntax: type name []; or type [packed_dim] name []; integer mem []; // dynamic array of integers bit [3:0] nibble []; // dynamic array of 4-bit vectors string names []; // dynamic array of strings byte payload[]; // dynamic array of bytes // After declaration, before new[]: // - size() == 0 // - any access returns type default value // - do NOT try to write — write is silently ignored $display(mem.size()); // prints 0 $display(mem[0]); // prints 'X (integer default, no error)
⚙ Allocating with new[]
The new[N] operator allocates exactly N elements. Elements are initialised to their type’s default value ('X for 4-state, '0 for 2-state, "" for string, null for class).
integer addr[]; // Basic allocation — all elements initialised to 'X (integer = 4-state) addr = new[100]; $display(addr.size()); // 100 $display(addr[0]); // 'X (uninitialized integer) // Allocating with 2-state type — defaults to '0 int vals[]; vals = new[50]; $display(vals[0]); // 0 (int = 2-state, defaults to 0) // Using a runtime expression for size int packet_len = 64; byte pkt[]; pkt = new[packet_len]; // size from a variable — fully legal // Can also allocate with a function call result int data[]; data = new[$urandom_range(8,64)]; // random size 8..64
Default values after allocation
| Element type | Default after new[] | Example |
|---|---|---|
4-state (integer, logic) | ‘X | integer a[]; a=new[4]; // a[0]='X |
2-state (int, bit, byte) | ‘0 | int b[]; b=new[4]; // b[0]=0 |
real, shortreal | 0.0 | real r[]; r=new[4]; // r[0]=0.0 |
string | “” | string s[]; s=new[4]; // s[0]="" |
| class handle | null | MyClass c[]; c=new[4]; // c[0]=null |
↕ Growing & Shrinking
The real power of dynamic arrays is resizing while preserving existing values. Pass the old array as the second argument to new[]:
new[ new_size ]( old_array )
integer addr[]; addr = new[100]; // 100 elements foreach(addr[i]) addr[i] = i; // fill with 0,1,2...99 // Grow to 200 — keep first 100 values, new 100 get 'X addr = new[200](addr); // addr[0] = 0 (preserved) // addr[99] = 99 (preserved) // addr[100]= 'X (new element, uninitialised) // Shrink to 50 — only first 50 values kept addr = new[50](addr); // addr[0] = 0 (preserved) // addr[49]= 49 (preserved) // addr[50] is gone // Quadruple the size addr = new[addr.size()*4](addr);
new[N](old), SystemVerilog copies min(old.size(), N) elements from old into the new allocation. If N is larger, extra elements get the type default. If N is smaller, extra old elements are simply not copied. The old array storage is then replaced by the new allocation — there is no separate old array afterwards.
📋 Built-in Methods: size(), delete()
Dynamic arrays have three built-in operations: the new[] allocation operator and two methods.
(old) copies existing values first. This is an operator, not a method.$size(arr, 1).int ab[] = new[8]; $display(ab.size()); // 8 ab = new[ab.size() * 4](ab); // quadruple: size = 32 $display(ab.size()); // 32 ab.delete(); $display(ab.size()); // 0 $display(ab[0]); // 0 (int default, no error)
ab.size() and $size(ab, 1) return exactly the same value. The method form is preferred for dynamic arrays because it reads naturally in code. The system function form is preferred in parameterised code that must work for both fixed-size and dynamic arrays.
✍ Initialising at Declaration
You can initialise a dynamic array directly in its declaration using an array literal. This implicitly calls new[] and sets the size from the literal.
// Literal init at declaration — size = number of values int d1[] = '{10, 20, 30, 40}; // 4 elements: 10,20,30,40 byte d2[] = '{8'hAA, 8'hBB}; // 2 elements string d3[] = '{"hello", "world"}; // 2 string elements // Access like any array $display(d1[0]); // 10 $display(d1.size()); // 4 // Combine declaration with replication int zeros[] = '{8{0}}; // 8 zeros
🆕 Common Patterns
Here are the most frequently used dynamic array patterns in real SystemVerilog testbenches.
Pattern 1 — Variable-length packet payload
class Packet; rand int unsigned len; byte payload[]; // dynamic — size set after randomise constraint c_len { len inside {[1:1500]}; } function void post_randomize(); payload = new[len]; // allocate after len is decided foreach(payload[i]) payload[i] = $urandom(); // fill with random bytes endfunction endclass Packet p = new(); p.randomize(); $display("Packet length: %0d", p.payload.size());
Pattern 2 — Building a result list during a search
function automatic int[] find_matches( input int haystack[], input int needle ); int result[]; int count = 0; // First pass: count matches foreach(haystack[i]) if(haystack[i] == needle) count++; // Allocate exactly what we need result = new[count]; // Second pass: fill result int j = 0; foreach(haystack[i]) if(haystack[i] == needle) result[j++] = i; return result; endfunction
Pattern 3 — Growing a log buffer
string log_entries[]; int log_count = 0; task log(string msg); // Grow the log by 1, preserving all previous entries log_entries = new[log_count+1](log_entries); log_entries[log_count] = msg; log_count++; endtask log("Test started"); log("Packet sent"); log("Response received"); // log_entries = {"Test started","Packet sent","Response received"}
string q[$]) with q.push_back(msg), which is the idiomatic SV approach for this pattern.
→ Array Assignment Rules
Assignment between arrays is legal only when types and sizes are compatible. The rules depend on whether both sides are fixed-size, whether one is dynamic, and whether the assignment is checked at compile time or at runtime.
The compatibility table
| Source | Destination | Result | When checked |
|---|---|---|---|
| Fixed-size, same size & compatible type | Fixed-size | ✓ OK | Compile time |
| Fixed-size, different size | Fixed-size | ✗ Error | Compile time |
| Dynamic, same size at runtime | Fixed-size (1-D) | ⚠ OK if sizes match | Runtime |
| Dynamic, different size | Fixed-size | ✗ Runtime error | Runtime |
| Fixed-size or dynamic (compatible type) | Dynamic | ✓ OK — dest resizes | Always valid |
| Wire array (same shape) | Variable array | ✓ OK | Compile time |
Fixed-to-fixed: size must match exactly
int A[10:1]; // 10 elements int B[0:9]; // 10 elements (different range, same size) int C[24:1]; // 24 elements A = B; // OK — same element count (10), element-by-element copy A = C; // COMPILE ERROR — different size (10 vs 24)
int A[10:1] and int B[0:9] are compatible because both have 10 elements. The actual index range (10:1 vs 0:9) is irrelevant for assignment compatibility. Elements are copied by position: A[10] gets B[0], A[9] gets B[1], and so on.
Wire array ↔ variable array
wire [31:0] W [9:0]; // wire array, same shape as A assign W = A; // OK — continuous assign of array initial #10 B = W; // OK — procedural read of wire array
Fixed-to-dynamic: destination always resizes
int fixed10[10:1]; // 10 elements int dyn[]; // empty dyn = fixed10; // dyn now has 10 elements — always legal $display(dyn.size()); // 10
Dynamic-to-fixed: requires runtime size match
int A[100:1]; // fixed, 100 elements int B[] = new[100]; // dynamic, 100 elements int C[] = new[8]; // dynamic, 8 elements A = B; // OK at runtime — sizes match (100 == 100) A = C; // RUNTIME ERROR — sizes don't match (100 != 8)
Slice and concatenation on the right-hand side
// You can build a new dynamic array from slices and extra elements string src[1:5] = {"a", "b", "c", "d", "e"}; string dst[]; dst = {src[1:3], "hello", src[4:5]}; // dst = {"a", "b", "c", "hello", "d", "e"} (6 elements) // Insert an element at position 2 of a dynamic array int v[] = '{10,20,30,40}; v = {v[0:1], 99, v[2:$size(v)-1]}; // v = {10, 20, 99, 30, 40}
src[1:5] = { "a", "b", "c", "d", "e" }
[1] [2] [3] [4] [5]
dst = { src[1:3], "hello", src[4:5] }
"a","b","c" "d","e"
dst = { "a", "b", "c", "hello", "d", "e" }
[0] [1] [2] [3] [4] [5]
Note: dst is a DYNAMIC array — it automatically sized to 6 elements.
📋 Arrays as Arguments
Arrays can be passed to tasks and functions. By default they are passed by value — a copy is made and changes inside the subroutine do not affect the caller. Use ref to pass by reference when you want changes to propagate back or want to avoid copying large arrays.
Default: pass by value (copy)
task print_arr(int arr[4]); // formal: fixed-size 4 elements foreach(arr[i]) $display(arr[i]); arr[0] = 999; // changes copy — caller unchanged endtask int myArr[4] = '{1,2,3,4}; print_arr(myArr); $display(myArr[0]); // still 1 — not modified
Pass by reference with ref
// ref: no copy — any change in the task is visible to caller task automatic fill_array(ref int arr[], input int n); arr = new[n]; foreach(arr[i]) arr[i] = i * i; endtask int result[]; fill_array(result, 5); // result is now {0,1,4,9,16} $display(result.size()); // 5 — the new[] inside task took effect
Fixed-size formal parameter rules
When the formal parameter is a fixed-size array, the actual argument must match in element count and number of dimensions. The exact range does not need to match — only the size matters.
task fun(int a[3:1][3:1]); // 3×3 array of int endtask int b1[3:1][3:1]; fun(b1); // OK — exact match int b2[1:3][0:2]; fun(b2); // OK — same SIZE (3×3), different range reg b3[3:1][3:1]; fun(b3); // OK — reg is assignment-compatible with int // event b4[3:1][3:1]; fun(b4); // ERROR — incompatible type // int b5[3:1]; fun(b5); // ERROR — wrong dimension count // int b6[3:1][4:1]; fun(b6); // ERROR — size mismatch (3 vs 4)
Dynamic formal parameter rules
A dynamic array formal parameter accepts any 1-D array of a compatible type, regardless of size. It also accepts fixed-size arrays.
task foo(string arr[]); // dynamic formal — accepts any 1-D string array endtask string fixed4[4]; foo(fixed4); // OK string fixed100[100]; foo(fixed100);// OK — different size, still OK string dyn[] = new[7]; foo(dyn); // OK string dyn2[] = new[1000]; foo(dyn2); // OK // int bad[] = new[7]; foo(bad); // ERROR — wrong element type
Passing a fixed-size to a task expecting a dynamic formal
task bar(string arr[4:1]); // fixed-size formal: exactly 4 strings endtask string s1[4:1]; bar(s1); // OK string s2[5:2]; bar(s2); // OK — same size, different range string s3[] = new[4]; bar(s3); // OK — dynamic with right size (runtime check) string s4[] = new[7]; bar(s4); // RUNTIME ERROR — size mismatch
ref when the array is large or when the called task needs to resize it. Use const ref if you want the efficiency of a reference without allowing modifications.
const ref — efficient read-only argument
// const ref: no copy (efficient), but task cannot modify the array task automatic checksum(const ref byte data[], output byte crc); crc = 0; foreach(data[i]) crc ^= data[i]; // data[0] = 0; // COMPILE ERROR — const ref cannot be written endtask byte payload[] = new[256]; byte crc_out; checksum(payload, crc_out); // no copy — payload passed by reference
📋 Quick Reference
Dynamic array operations at a glance
| Operation | Syntax | Result |
|---|---|---|
| Declare | int d[]; | Empty array, size()=0 |
| Allocate | d = new[N]; | N elements, type default |
| Allocate + copy | d = new[N](old); | N elements, first min(N,old.size()) from old |
| Check size | d.size() | Current element count (0 if unallocated) |
| Free all | d.delete(); | size() becomes 0 |
| Init with literal | int d[] = ‘{1,2,3}; | 3 elements: 1, 2, 3 |
Assignment compatibility summary
| From → To | Legal? | Notes |
|---|---|---|
| Fixed → Fixed (same size) | ✓ Compile time | Range can differ; size must match |
| Fixed → Fixed (diff size) | ✗ Compile error | Size mismatch caught at compile time |
| Fixed → Dynamic | ✓ Always | Dynamic resizes to match fixed size |
| Dynamic → Dynamic | ✓ Always | Dest resizes to match source size |
| Dynamic → Fixed (same size) | ⚠ Runtime check | Error if sizes differ at runtime |
| Dynamic → Fixed (diff size) | ✗ Runtime error | Cannot be detected at compile time |
Argument-passing summary
| Formal parameter | Accepts | Default passing |
|---|---|---|
| Fixed-size | Same size (any compatible type, any range) | By value (copy) |
Dynamic [] | Any 1-D array of compatible type, any size | By value (copy) |
ref any array | Exact match required | By reference (no copy) |
const ref | Same as ref | By reference, read-only inside task |
