SYSTEMVERILOG SERIES · SV-04B

SystemVerilog Series — SV-04b: Dynamic Arrays — VLSI Trainers
SystemVerilog Series · SV-04b

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
Key difference from fixed-size: A fixed-size array is always allocated — it lives for the lifetime of its scope. A dynamic array starts as an empty zero-size array and grows only when you call 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 typeDefault after new[]Example
4-state (integer, logic)‘Xinteger a[]; a=new[4]; // a[0]='X
2-state (int, bit, byte)‘0int b[]; b=new[4]; // b[0]=0
real, shortreal0.0real r[]; r=new[4]; // r[0]=0.0
string“”string s[]; s=new[4]; // s[0]=""
class handlenullMyClass 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);
Fig 1 — State after each new[] call (6 elements for clarity)
After new[6]
X
X
X
X
X
X
all ‘X (integer default)
Fill 0..5
0
1
2
3
4
5
filled with values
new[9](old)
0
1
2
3
4
5
X
X
X
blue = copied, orange = new ‘X
new[4](old)
0
1
2
3
shrunk — elements 4,5 discarded
delete()
empty
size() = 0
How copy-on-grow works precisely: When you write 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.

new[N]
operator
Allocate N elements initialised to type default. Optional second arg (old) copies existing values first. This is an operator, not a method.
size()
function int
Returns the current number of elements. Returns 0 if the array has not been allocated yet. Equivalent to $size(arr, 1).
delete()
function void
Empties the array — size drops to 0 and all storage is released. After delete(), size() returns 0 and element access returns the type default.
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)
size() vs $size(): 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"}
Growing one-at-a-time is slow for large arrays. If you are appending many elements, grow by a larger chunk (e.g., double the size each time like a vector) to avoid O(N²) copy overhead. Alternatively, use a queue (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)
Range doesn’t matter, size does: 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}
Fig 2 — Slice + concat: building dst from slices of src and an extra element
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
Passing dynamic arrays by ref is almost always better for large arrays. When you pass a dynamic array by value, the entire array is copied. For a 10,000-element array this is expensive in both time and memory. Pass by 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

OperationSyntaxResult
Declareint d[];Empty array, size()=0
Allocated = new[N];N elements, type default
Allocate + copyd = new[N](old);N elements, first min(N,old.size()) from old
Check sized.size()Current element count (0 if unallocated)
Free alld.delete();size() becomes 0
Init with literalint d[] = ‘{1,2,3};3 elements: 1, 2, 3

Assignment compatibility summary

From → ToLegal?Notes
Fixed → Fixed (same size)✓ Compile timeRange can differ; size must match
Fixed → Fixed (diff size)✗ Compile errorSize mismatch caught at compile time
Fixed → Dynamic✓ AlwaysDynamic resizes to match fixed size
Dynamic → Dynamic✓ AlwaysDest resizes to match source size
Dynamic → Fixed (same size)⚠ Runtime checkError if sizes differ at runtime
Dynamic → Fixed (diff size)✗ Runtime errorCannot be detected at compile time

Argument-passing summary

Formal parameterAcceptsDefault passing
Fixed-sizeSame size (any compatible type, any range)By value (copy)
Dynamic []Any 1-D array of compatible type, any sizeBy value (copy)
ref any arrayExact match requiredBy reference (no copy)
const refSame as refBy reference, read-only inside task
Coming next: The next article covers Associative Arrays and Queues — the two remaining variable-size array kinds: the sparse hash-map indexed by any type, and the O(1) push/pop queue.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top