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
📌 Index Types
The index type goes inside the brackets. Five kinds are supported:
| Index type | Declaration example | Index ordering | Notes |
|---|---|---|---|
| * | 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 type | Returned when key missing |
|---|---|
4-state integral (integer, logic) | ‘X (unknown) |
2-state integral (int, bit, byte) | ‘0 (zero) |
| real / shortreal | 0.0 |
| string | “” (empty string) |
| class handle | null |
| event | null |
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.
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
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}
$ 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.
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
$+1is 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.
Q={Q[0:i-1],item,Q[i:$]}.Q={Q[0:i-1],Q[i+1:$]}.Q={item,Q}.Q={Q,item}.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
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.
// 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
🔎 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.
with expression is true.with expression).with expression).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.
with clause sorts by a computed key.with clause.$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
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.
with expression).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
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.
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);
