SYSTEMVERILOG SERIES · SV-04C

SystemVerilog Series — SV-04c: Associative Arrays, Queues & Manipulation Methods — VLSI Trainers
SystemVerilog Series · SV-04c

Associative Arrays, Queues & Array Manipulation Methods

The three remaining array kinds — associative arrays (sparse hash maps), queues (variable-size FIFO lists), and the full suite of array manipulation methods for searching, ordering, and reducing — with worked examples at every step.

🔑 Associative Arrays

An associative array is a sparse hash map. Unlike fixed or dynamic arrays that allocate a block of N contiguous storage slots, an associative array only allocates storage for entries that have been explicitly written. This makes it ideal when the index space is large but only a few locations will actually be used.

Fixed array: always allocates all slots

// 1 million integers always in memory
// even if only 3 are ever written
int mem[1_000_000];
mem[0]      = 10;
mem[500000] = 20;
mem[999999] = 30;
// 999,997 slots wasted

Associative array: allocates only what is written

// Only 3 entries ever exist in memory
// no matter how large the index space
int mem[*];    // wildcard index: any integral
mem[0]      = 10;
mem[500000] = 20;
mem[999999] = 30;
// mem.num() == 3
The killer use case: sparse memory models. In verification you often need to model a large memory (e.g. 4 GB address space) but a typical test only touches a few hundred locations. An associative array indexed by address uses only as much memory as the test actually exercises — typically kilobytes instead of gigabytes.

📌 Index Types

The index type goes inside the brackets. Five kinds are supported:

Index typeDeclaration exampleIndex orderingNotes
* int a[*]; Numerical (unsigned) Any integral type; X/Z values are invalid indices
string int a[string]; Lexicographic Empty string "" is a valid key
integer int a[integer]; Signed numerical Smaller indices sign-extended to 32 bits
User-defined type int a[MyClass]; Implementation-defined null is a valid key; derived class handles allowed
// All legal associative array declarations
integer    mem     [*];           // wildcard: any integral index
bit[20:0]  arr_b   [string];     // string-keyed
int        score   [string];     // scoreboard indexed by name
int        lookup  [integer];   // signed integer key
string     desc    [int];        // unsigned 32-bit key
event      ev_arr  [MyClass];   // class handle key

📌 Accessing Non-Existent Entries

Reading a key that has never been written returns the type’s default value — no crash, no error, just a silent default. Always use exists() before reading if you need to distinguish “never written” from “written to the default value”.

Element typeReturned when key missing
4-state integral (integer, logic)‘X (unknown)
2-state integral (int, bit, byte)‘0 (zero)
real / shortreal0.0
string“” (empty string)
class handlenull
eventnull
int     score[string];
integer mem  [*];

score["Alice"] = 95;
$display(score["Alice"]);  // 95  (entry exists)
$display(score["Bob"]);    // 0   (int default — Bob not written)
$display(mem[9999]);       // 'X  (integer default — never written)

// Safe pattern: always check before reading if you need certainty
if (score.exists("Bob"))
  $display("Bob scored %0d", score["Bob"]);
else
  $display("Bob has no score yet");

🔨 Associative Array Methods

Seven built-in methods let you query size, check membership, delete entries, and traverse in order.

num()
function int
Number of entries currently stored. 0 if empty.
exists(idx)
function int
Returns 1 if an entry exists for this key, 0 otherwise. Does NOT create the entry.
delete([idx])
function void
Remove one entry (with argument) or all entries (no argument).
first(ref idx)
function int
Sets idx to the smallest key. Returns 0 if array is empty.
last(ref idx)
function int
Sets idx to the largest key. Returns 0 if array is empty.
next(ref idx)
function int
Sets idx to next larger key. Returns 0 if at end.
prev(ref idx)
function int
Sets idx to next smaller key. Returns 0 if at start.

Complete walkthrough example

int map[string];

// Populate
map["charlie"] = 3;
map["alice"]   = 1;
map["bob"]     = 2;

$display("num = %0d", map.num());     // 3
$display("exists alice: %0b", map.exists("alice"));  // 1
$display("exists dave:  %0b", map.exists("dave"));   // 0

// Forward iteration: alice → bob → charlie (lexicographic)
string k;
if(map.first(k))
  do
    $display("%s = %0d", k, map[k]);
  while(map.next(k));
// Output:  alice = 1 / bob = 2 / charlie = 3

// Backward iteration
if(map.last(k))
  do
    $display("%s = %0d", k, map[k]);
  while(map.prev(k));
// Output:  charlie = 3 / bob = 2 / alice = 1

// Delete one entry
map.delete("bob");
$display(map.num());   // 2

// Delete ALL entries
map.delete();
$display(map.num());   // 0
The exists() trap: Simply reading map["dave"] returns 0 but does NOT create a “dave” entry — the read is non-destructive. However, assigning to a key that doesn’t exist does create it: map["dave"] += 1 first reads 0 (default), adds 1, then writes 1 — creating the “dave” entry. If you want to increment only for existing keys, guard with if(map.exists("dave")) first.

Pattern: sparse memory model

class SparseMemory;
  bit [7:0] mem[*];        // wildcard index: any address

  function void write(int unsigned addr, bit[7:0] data);
    mem[addr] = data;
  endfunction

  function bit[7:0] read(int unsigned addr);
    if (mem.exists(addr)) return mem[addr];
    else                  return 8'hFF; // uninitialized = 0xFF
  endfunction

  function void dump();
    int unsigned a;
    if(mem.first(a))
      do
        $display("[%0h] = %0h", a, mem[a]);
      while(mem.next(a));
  endfunction
endclass

📄 Associative Literals & Assignment

Associative arrays can be initialised with a literal that specifies key-value pairs and an optional default value for all unspecified keys.

// Literal with explicit key:value pairs + default
integer table[string] = {"Peter":20, "Paul":22, "Mary":23, default:-1};
// table["Peter"] = 20, table["Paul"] = 22, table["Mary"] = 23
// any other key returns -1 (the default)

string words[int] = {default: "unknown"};
// Every key returns "unknown" until explicitly written
words[5] = "five";
$display(words[5]);    // "five"  (explicitly set)
$display(words[9]);    // "unknown"  (hits default)

// Assignment copies the entire map (deep copy)
int src[string] = {"a":1, "b":2};
int dst[string];
dst = src;             // dst gets its own copy — changes to dst don't affect src
dst["c"] = 3;
$display(src.num());   // 2 — src unchanged
$display(dst.num());   // 3

// Passing to functions: also passed by value (copy) by default
task print_map(int m[string]);    // copy made on call
endtask
task modify_map(ref int m[string]); // reference: changes visible to caller
endtask

🆕 Queues

A queue is an ordered, variable-size collection. It combines the element-access of an unpacked array with the ability to grow and shrink at both ends in constant time. The special symbol $ always refers to the index of the last element.

// Syntax: type name[$];          — unbounded
//         type name[$:MAXSIZE];  — bounded

byte    q1[$];                    // unbounded byte queue
string  names[$]  = {"Bob"};     // initialised with one element
integer Q[$]      = {3,2,7};    // initialised with three elements
bit     q2[$:255];              // bounded: max 256 elements

// Reading
int first = Q[0];   // first element = 3
int last  = Q[$];   // last element  = 7
int mid   = Q[1];   // middle element = 2

// Writing a single element
Q[0] = 99;          // {99, 2, 7}
$ as a dynamic index: $ evaluates to the index of the last element. For an empty queue it evaluates to -1, so Q[$] on an empty queue returns the type default. Always check Q.size() > 0 before using Q[$] or Q[0].

Queue Operators — Slice & Concatenation Syntax

All queue insert, remove, and append operations are expressed using ordinary concatenation {} and slice [a:b] syntax. No special operator is needed.

Fig 1 — Queue state after each operation
Starting: Q = {2, 4, 8}
Q = {2,4,8}
2
4
8
initial state
Q={Q, 6}
2
4
8
6
append 6 to back
Q={1, Q}
1
2
4
8
6
prepend 1 to front
Q=Q[1:$]
1
2
4
8
6
remove first — red=removed
Q=Q[0:$-1]
2
4
8
6
remove last — red=removed
insert at [1]
2
99
4
8
purple=inserted element
Q = {}
empty
clear: assign empty braces
int Q[$] = {2, 4, 8};

// Append (push to back)
Q = {Q, 6};              // {2,4,8,6}

// Prepend (push to front)
Q = {1, Q};              // {1,2,4,8,6}

// Remove first element
Q = Q[1:$];             // {2,4,8,6}

// Remove last element
Q = Q[0:$-1];           // {2,4,8}

// Insert at position 1
int pos=1, val=99;
Q = {Q[0:pos-1], val, Q[pos:$]}; // {2,99,4,8}

// Delete element at position 2
Q = {Q[0:1], Q[3:$]};   // {2,99,8}

// Clear — assign empty braces
Q = {};                   // {}

// Append another queue's slice
int A[$]={1,2,3}, B[$]={4,5,6};
A = {A, B[1:2]};           // {1,2,3,5,6}  (B[1] and B[2] appended)

Slice rules for queues

  • Q[a:b] — yields b-a+1 elements. If a > b, yields empty queue {}.
  • Q[n:n] — one-element queue containing Q[n].
  • If n is out of range, Q[n:n] yields {}.
  • If a < 0 it clamps to 0; if b > $ it clamps to $.
  • Writing to index $+1 is legal — appends to the queue.
  • Writing past a bounded limit is silently ignored (with a tool warning).

📋 Queue Built-In Methods

Six methods plus size() provide a cleaner, less verbose interface to the most common queue operations.

size()
function int
Current number of elements. 0 if empty.
insert(i, item)
task
Insert item before index i. Equivalent to Q={Q[0:i-1],item,Q[i:$]}.
delete(i)
task
Delete element at index i. Equivalent to Q={Q[0:i-1],Q[i+1:$]}.
push_front(item)
task
Insert at front. Equivalent to Q={item,Q}.
push_back(item)
task
Append at back. Equivalent to Q={Q,item}.
pop_front()
function T
Remove and return first element. Queue shrinks by 1.
pop_back()
function T
Remove and return last element. Queue shrinks by 1.
int q[$] = {10, 20, 30};

q.push_back(40);            // {10,20,30,40}
q.push_front(5);           // {5,10,20,30,40}

int f = q.pop_front();      // f=5,  q={10,20,30,40}
int b = q.pop_back();       // b=40, q={10,20,30}

q.insert(1, 15);             // {10,15,20,30}  — 15 inserted before index 1
q.delete(2);                 // {10,15,30}     — element at index 2 removed

$display("size = %0d", q.size()); // 3
Queue as a testbench FIFO: The most common pattern is a producer-consumer FIFO. The producer calls q.push_back(item); the consumer polls q.size() > 0 and calls item = q.pop_front(). For multi-threaded testbenches where both run concurrently, wrap with a semaphore or use a mailbox which provides built-in synchronisation.

Pattern: scoreboard using a queue

class Scoreboard;
  int expected[$];         // queue of expected values

  task predict(int val);
    expected.push_back(val);  // enqueue expected result
  endtask

  task check(int actual);
    if (expected.size() == 0)
      $error("Unexpected output: %0d", actual);
    else begin
      int exp = expected.pop_front();
      if (actual !== exp)
        $error("Mismatch: got %0d, expected %0d", actual, exp);
      else
        $display("PASS: %0d", actual);
    end
  endtask
endclass

🔨 Array Manipulation Methods

Array manipulation methods work on any unpacked array or queue. They provide searching, sorting, and reducing without requiring manual loops. All accept an optional with (expression) clause that customises how elements are compared or combined.

General syntax
// array.method_name [ (iterator) ] [ with (expression) ]
// Default iterator name: item
arr.find()           with (item > 5)     // default iterator: item
arr.find(x)          with (x > 5)        // custom iterator name: x
arr.sort()           with (item.key)     // sort by struct field
arr.sum()                                 // no with: sum all elements
Three families of methods: Locator — search and filter, return a queue of results. Ordering — sort, reverse, shuffle in-place. Reduction — collapse the array to a single value.

🔎 Locator Methods

Locator methods search the array and return a queue of matching elements or their indexes. The with clause is mandatory for find/find_* methods; optional for min/max/unique.

Locator
find()
returns queue of elements
All elements for which the with expression is true.
find_index()
returns queue of int
Indexes of all matching elements.
find_first()
returns queue (0 or 1 element)
The first element satisfying the expression.
find_first_index()
returns queue of int
Index of the first matching element.
find_last()
returns queue (0 or 1 element)
The last element satisfying the expression.
find_last_index()
returns queue of int
Index of the last matching element.
min()
returns queue of elements
Element(s) with the minimum value (or minimum of with expression).
max()
returns queue of elements
Element(s) with the maximum value.
unique()
returns queue of elements
One copy of each unique value (or unique with expression).
unique_index()
returns queue of int
Indexes of elements with unique values.
int    nums[] = '{3,1,4,1,5,9,2,6};
int    qi[$];
string SA[5] = {"cat","apple","zebra","apple","mango"};
string qs[$];

// find: all elements > 4
qi = nums.find with (item > 4);
// qi = {5, 9, 6}

// find_index: indexes where value == 1
qi = nums.find_index with (item == 1);
// qi = {1, 3}  (positions of the two 1s)

// find_first: first element > 5
qi = nums.find_first with (item > 5);
// qi = {9}  (first element > 5 is 9 at index 5)

// min and max (no with needed for integers)
qi = nums.min;   // qi = {1}
qi = nums.max;   // qi = {9}

// unique: remove duplicates
qs = SA.unique;
// qs = {"cat","apple","zebra","mango"}  (one "apple")

// unique with case-insensitive comparison
qs = SA.unique(s) with (s.tolower());
// Same result, but treats "Apple" and "apple" as the same

// Struct example: find by a specific field
typedef struct { int id; string name; } person_t;
person_t people[] = '{{1,"Alice"},{2,"Bob"},{3,"Charlie"}};
person_t qp[$];

qp = people.find(p) with (p.id > 1);
// qp = {{2,"Bob"},{3,"Charlie"}}

// item.index() — the current element's index
qi = nums.find_index with (item == item.index());
// finds positions where the value equals the index itself

🔄 Ordering Methods

Ordering methods modify the array in place. They do not return a value. sort() and rsort() accept an optional with clause to sort by a computed key; reverse() and shuffle() do not.

Ordering
sort()
ascending order
Sort in-place, smallest first. Optional with clause sorts by a computed key.
rsort()
descending order
Sort in-place, largest first. Optional with clause.
reverse()
no with allowed
Reverse element order in-place. Works on packed arrays too.
shuffle()
no with allowed
Randomise element order in-place. Uses the same RNG as $random.
int q[$] = {4, 5, 3, 1, 2};

q.sort;         // {1,2,3,4,5}  — ascending
q.rsort;        // {5,4,3,2,1}  — descending
q.reverse;      // {1,2,3,4,5}  — reversed again
q.shuffle;      // {3,1,5,2,4}  — random order (example)

// reverse also works on packed arrays
logic [3:0] b = 4'bXZ01;
b.reverse;      // b = 4'b10ZX

// Sort structs by a field
typedef struct { int score; string name; } entry_t;
entry_t board[] = '{{90,"Alice"},{70,"Bob"},{85,"Carol"}};

board.sort(e) with (e.score);
// board = {{70,"Bob"},{85,"Carol"},{90,"Alice"}}  — ascending score

board.rsort(e) with (e.score);
// board = {{90,"Alice"},{85,"Carol"},{70,"Bob"}}  — descending score

// Sort by computed key: length of name string
string names[] = '{"Zephyr","Bob","Alice","Li"};
names.sort(n) with (n.len());
// {"Li","Bob","Alice","Zephyr"}  — shortest name first
reverse() and shuffle() never accept a with clause. If you add with (...) to either, it is a compile error. sort() and rsort() without a with clause require the element type to have relational operators defined; for integers and strings this is automatic, for custom structs you must provide a with clause.

Reduction Methods

Reduction methods collapse the entire array into a single value. The with clause transforms each element before the reduction is applied.

Reduction
sum()
Sum of all elements (or sum of with expression).
product()
Product of all elements.
and()
Bitwise AND of all elements.
or()
Bitwise OR of all elements.
xor()
XOR of all elements. Useful as a simple checksum.
byte b[] = '{1, 2, 3, 4};

b.sum            // 10  (1+2+3+4)
b.product        // 24  (1*2*3*4)
b.and             // 0   (1&2&3&4 = 0000_0000)
b.or              // 7   (1|2|3|4 = 0000_0111)
b.xor             // 4   (1^2^3^4 = 0000_0100)

// with clause: transform before reducing
b.sum with (item + 4)    // (5)+(6)+(7)+(8) = 26
b.xor with (item * 2)    // 2^4^6^8 = XOR of doubled values

// Type coercion to avoid overflow
b.sum              // result type is BYTE — can overflow for large arrays!
int'(b.sum)         // cast to int before summing — avoids overflow

// Real-world: check if all bytes in a packet are non-zero
byte pkt[];
if (pkt.and != 0)
  $display("No zero bytes in packet");

// Checksum: XOR of all payload bytes
byte crc = pkt.xor;

// Count elements satisfying a condition using sum with 1-bit expression
int count_gt5 = int'(nums.sum(n) with (n > 5 ? 1 : 0));
// counts how many elements are > 5
Counting elements with sum(): A particularly useful pattern is combining sum() with a boolean with expression to count how many elements satisfy a condition: nums.sum(n) with (n > 5 ? 1 : 0) This sums 1 for each element > 5 and 0 for others — giving the total count. Cast to int first to avoid overflow on large arrays.
No side effects in with clauses. Array manipulation methods traverse elements in unspecified order. If your with expression increments a counter or writes to a variable, the result is undefined. Always keep with expressions pure — read-only, no assignments, no function calls with side effects.

Combining methods: a complete worked example

// Task: from a list of test results, find the top 3 unique scores,
// sorted highest to lowest, with their sum.

int results[] = '{85,92,78,92,65,78,99,88};
int top3[$];
int total;

// Step 1: get unique scores
top3 = int'(results.unique());  // {85,92,78,65,99,88}

// Step 2: sort descending
top3.rsort;                       // {99,92,88,85,78,65}

// Step 3: keep only first 3
top3 = top3[0:2];                // {99,92,88}

// Step 4: sum them
total = int'(top3.sum);           // 279

$display("Top 3: %0d %0d %0d — Sum: %0d",
  top3[0], top3[1], top3[2], total);

Leave a Comment

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

Scroll to Top