SystemVerilog · Module 3
Array Methods
sort, rsort, find, find_index, sum, min, max, unique — with-clause expressions.
Module 3 · Page 3.5
The Loop You Should Stop Writing Manually
Before array methods existed in SystemVerilog, finding the maximum value in an array required a foreach loop with a running maximum variable — five lines for something that should be one. Sorting required building or importing a sort function. Filtering elements by a condition required another loop with a temporary array.
The array reduction and locator methods introduced in SV-2005 and extended in later revisions eliminate most of that boilerplate. They work uniformly across fixed-size arrays, dynamic arrays, and queues — write the method once, use it on any compatible array type. The return types are consistent: locator methods return queues, in-place methods modify the original array directly.
The key concept that unlocks the full power of these methods is the with clause. arr.sum() sums all elements. arr.sum() with (item.data) sums just the data field of each struct element. arr.find() with (item > threshold) returns all elements greater than a threshold. The item keyword inside the with clause represents the current element — exactly like a lambda expression in other languages.
Three Method Families — Know Which Is Which
Ordering methods (in-place)
sort(), rsort(), shuffle() — modify the array directly. No return value. The original array is reordered after the call.
Locator methods (return queue)
find(), find_first(), find_last(), find_index(), min(), max(), unique(), unique_index() — return a new queue. Original array unchanged.
Reduction methods (return scalar)
sum(), product() — return a single value of the same type as the array element (or as specified by the with clause).
with clause — the power multiplier
method() with (item.field) applies the method to a field of each element, or to a computed expression. item = current array element in the iteration.
The with clause is optional for most methods. Without it, the method operates on the elements directly. With it, the method operates on the expression you define — where item stands for each element as the method iterates. This lets you sort an array of structs by a specific field, find elements matching a computed condition, or sum a single field across all elements.
Complete Method Reference
int arr [] = '{50, 10, 40, 10, 30, 20, 40};
// ════════════════════════════════════════════════════════════════
// ORDERING METHODS — in-place, no return value
// ════════════════════════════════════════════════════════════════
arr.sort(); // '{10,10,20,30,40,40,50} — ascending
arr.rsort(); // '{50,40,40,30,20,10,10} — descending
arr.shuffle(); // '{??} — random permutation (uses $urandom)
// sort by a field of a struct
typedef struct { int id; int score; } rec_t;
rec_t recs [];
recs.sort() with (item.score); // sort recs by .score field ascending
recs.rsort() with (item.id); // sort recs by .id field descending
// ════════════════════════════════════════════════════════════════
// LOCATOR METHODS — return queue of matching elements/indices
// ════════════════════════════════════════════════════════════════
// find — ALL elements matching condition
int above30 [$] = arr.find() with (item > 30); // '{40,40,50}
int eq10 [$] = arr.find() with (item == 10); // '{10,10}
// find_first / find_last — one element
int first_big [$] = arr.find_first() with (item > 30); // '{40}
int last_big [$] = arr.find_last() with (item > 30); // '{50}
// find_index — indices of matching elements
int idx_above30 [$] = arr.find_index() with (item > 30); // '{2,4,0}
int first_idx [$] = arr.find_first_index() with (item > 30); // '{2}
int last_idx [$] = arr.find_last_index() with (item > 30); // '{0}
// min / max — queue containing the single min or max element
int min_val [$] = arr.min(); // '{10}
int max_val [$] = arr.max(); // '{50}
// unique / unique_index — remove duplicate values
int uniq [$] = arr.unique(); // '{50,10,40,30,20}
int uniq_idx[$] = arr.unique_index(); // first occurrence indices
// ════════════════════════════════════════════════════════════════
// REDUCTION METHODS — return scalar
// ════════════════════════════════════════════════════════════════
int total = arr.sum(); // 200 (10+10+20+30+40+40+50)
int product = arr.product(); // large number
// with clause: sum a specific field
int score_total = recs.sum() with (item.score); // sum of all .score fields
int cnt_above30 = arr.sum() with (item > 30); // count elements > 30 (each match contributes 1)| Method | Family | Returns | with clause? |
|---|---|---|---|
sort() | Ordering | void (in-place) | Optional — sort by expression |
rsort() | Ordering | void (in-place) | Optional — sort desc by expression |
shuffle() | Ordering | void (in-place) | Not supported |
find() | Locator | Queue of elements | Required — the filter condition |
find_first() | Locator | Queue (0 or 1 element) | Required |
find_last() | Locator | Queue (0 or 1 element) | Required |
find_index() | Locator | Queue of int indices | Required |
find_first_index() | Locator | Queue (0 or 1 int) | Required |
find_last_index() | Locator | Queue (0 or 1 int) | Required |
min() | Locator | Queue (1 element) | Optional — compare by field |
max() | Locator | Queue (1 element) | Optional — compare by field |
unique() | Locator | Queue of unique values | Optional — unique by field |
unique_index() | Locator | Queue of int indices | Optional |
sum() | Reduction | Scalar (element type) | Optional — sum expression |
product() | Reduction | Scalar (element type) | Optional |
Visual — What Each Method Returns
Input Array and Method Results at a Glance
Starting array: int arr [] = '{50, 10, 40, 10, 30, 20, 40}
| Method call | Result | Type | Notes |
|---|---|---|---|
arr.sort() | arr = '{10,10,20,30,40,40,50} | void — arr modified | In-place ascending |
arr.rsort() | arr = '{50,40,40,30,20,10,10} | void — arr modified | In-place descending |
arr.min() | '{10} | Queue of 1 | Access via [0] |
arr.max() | '{50} | Queue of 1 | Access via [0] |
arr.sum() | 200 | Scalar int | Direct assignment |
arr.find() with (item > 30) | '{50,40,40} | Queue | All matching elements |
arr.find_first() with (item > 30) | '{50} | Queue of 0 or 1 | First match only |
arr.find_index() with (item == 10) | '{1,3} | Queue of int | Indices, not values |
arr.unique() | '{50,10,40,30,20} | Queue | Order of first occurrence |
arr.sum() with (item > 30) | 3 | Scalar int | Counts elements matching condition |
The with Clause — How item Works
| Expression | What item is | Effect |
|---|---|---|
arr.sum() with (item) | Each int element | Sum of all elements (same as no-with) |
arr.sum() with (int'(item > 30)) | int (1 if element > 30, else 0) | Count of elements greater than 30 — the int' cast is required, see below |
recs.sum() with (item.score) | The .score field of each struct | Sum of all score fields |
recs.sort() with (item.id) | .id field used as sort key | Sort records by ID |
recs.find() with (item.score > 80) | Struct record where score > 80 | All records with score above 80 |
arr.max() with (item % 10) | item mod 10 — computed expression | Element with largest units digit |
Code Examples — Sorting Records to Scoreboard Analysis
Example 1 — Beginner: All Methods on Integer Arrays
module tb_methods_basic;
int arr [] = '{50, 10, 40, 10, 30, 20, 40};
initial begin
// ── Ordering ──────────────────────────────────────────────────
arr.sort();
$display("Sorted asc: %p", arr); // '{10,10,20,30,40,40,50}
arr.rsort();
$display("Sorted desc: %p", arr); // '{50,40,40,30,20,10,10}
// Reset for remaining tests
arr = '{50, 10, 40, 10, 30, 20, 40};
// ── Reduction ─────────────────────────────────────────────────
$display("sum = %0d", arr.sum()); // 200
$display("min = %0d", arr.min()[0]); // 10 (access [0] of returned queue)
$display("max = %0d", arr.max()[0]); // 50
// ── Count using sum with condition ────────────────────────────
$display("count > 30: %0d", arr.sum() with (item > 30)); // 3
$display("count == 10: %0d", arr.sum() with (item == 10)); // 2
// ── Locators ──────────────────────────────────────────────────
int big[$] = arr.find() with (item > 30);
$display("elements > 30: %p", big); // '{50,40,40}
int first[$] = arr.find_first() with (item > 30);
$display("first > 30: %0d", first[0]); // 50
int idx[$] = arr.find_index() with (item == 10);
$display("indices of 10: %p", idx); // '{1,3}
// ── Unique ────────────────────────────────────────────────────
int u[$] = arr.unique();
$display("unique: %p", u); // '{50,10,40,30,20}
// ── No match: find returns empty queue ───────────────────────
int none[$] = arr.find() with (item > 100);
$display("find > 100: size=%0d", none.size()); // 0
$finish;
end
endmoduleExpected output:
Sorted asc: '{10, 10, 20, 30, 40, 40, 50}
Sorted desc: '{50, 40, 40, 30, 20, 10, 10}
sum = 200
min = 10
max = 50
count > 30: 3
count == 10: 2
elements > 30: '{50, 40, 40}
first > 30: 50
indices of 10: '{1, 3}
unique: '{50, 10, 40, 30, 20}
find > 100: size=0Example 2 — Intermediate: Sorting and Filtering Structs
module tb_struct_methods;
typedef struct {
int tid;
logic [7:0] opcode;
int latency; // cycles from send to response
} txn_t;
txn_t log [];
initial begin
// Simulate 5 captured transactions with different latencies
log = new[5];
log[0] = '{0, 8'h10, 15};
log[1] = '{1, 8'h20, 3};
log[2] = '{2, 8'h10, 22};
log[3] = '{3, 8'h30, 8};
log[4] = '{4, 8'h20, 5};
// ── Sort by latency ascending ────────────────────────────────
log.sort() with (item.latency);
$display("Sorted by latency:");
foreach (log[i])
$display(" tid=%0d op=0x%02h lat=%0d", log[i].tid, log[i].opcode, log[i].latency);
// ── Find high-latency transactions ────────────────────────────
txn_t slow[$] = log.find() with (item.latency > 10);
$display("High latency (>10): %0d transactions", slow.size());
foreach (slow[i])
$display(" tid=%0d lat=%0d", slow[i].tid, slow[i].latency);
// ── Average latency using sum with ────────────────────────────
int total_lat = log.sum() with (item.latency);
$display("Average latency = %0d cycles", total_lat / log.size());
// ── Max latency ───────────────────────────────────────────────
txn_t worst[$] = log.max() with (item.latency);
$display("Worst: tid=%0d lat=%0d", worst[0].tid, worst[0].latency);
// ── Unique opcodes seen ───────────────────────────────────────
txn_t uniq_ops[$] = log.unique() with (item.opcode);
$display("Unique opcodes: %0d", uniq_ops.size());
foreach (uniq_ops[i])
$display(" 0x%02h", uniq_ops[i].opcode);
$finish;
end
endmoduleExpected output:
Sorted by latency:
tid=1 op=0x20 lat=3
tid=4 op=0x20 lat=5
tid=3 op=0x30 lat=8
tid=0 op=0x10 lat=15
tid=2 op=0x10 lat=22
High latency (>10): 2 transactions
tid=0 lat=15
tid=2 lat=22
Average latency = 10 cycles
Worst: tid=2 lat=22
Unique opcodes: 3
0x10
0x20
0x30Example 3 — Verification: Coverage Analysis and Scoreboard Summary
module tb_analysis;
typedef struct {
int txn_id;
logic [7:0] opcode;
int latency;
bit passed;
} result_t;
result_t results [];
initial begin
results = new[8];
results[0] = '{0, 8'h10, 5, 1};
results[1] = '{1, 8'h20, 12, 0}; // FAIL
results[2] = '{2, 8'h10, 4, 1};
results[3] = '{3, 8'h30, 18, 0}; // FAIL
results[4] = '{4, 8'h20, 7, 1};
results[5] = '{5, 8'h10, 3, 1};
results[6] = '{6, 8'h30, 9, 1};
results[7] = '{7, 8'h20, 25, 0}; // FAIL
// ── Pass/fail statistics ─────────────────────────────────────
int pass_cnt = results.sum() with (item.passed);
int fail_cnt = results.sum() with (!item.passed);
$display("PASS=%0d FAIL=%0d", pass_cnt, fail_cnt);
// ── Worst-case latency analysis ───────────────────────────────
result_t slow_q[$] = results.max() with (item.latency);
$display("Peak latency: tid=%0d lat=%0d cycles",
slow_q[0].txn_id, slow_q[0].latency);
// ── Find failing transactions and sort by latency ─────────────
result_t fails[$] = results.find() with (!item.passed);
fails.sort() with (item.latency);
$display("Failed transactions (sorted by latency):");
foreach (fails[i])
$display(" tid=%0d op=0x%02h lat=%0d",
fails[i].txn_id, fails[i].opcode, fails[i].latency);
// ── Unique opcodes that failed ────────────────────────────────
result_t fail_ops[$] = fails.unique() with (item.opcode);
$display("Distinct failing opcodes: %0d", fail_ops.size());
// ── Average latency of passing transactions only ──────────────
result_t passing[$] = results.find() with (item.passed);
int avg = passing.sum() with (item.latency) / passing.size();
$display("Avg passing latency = %0d cycles", avg);
$finish;
end
endmoduleExpected output:
PASS=5 FAIL=3
Peak latency: tid=7 lat=25 cycles
Failed transactions (sorted by latency):
tid=1 op=0x20 lat=12
tid=3 op=0x30 lat=18
tid=7 op=0x20 lat=25
Distinct failing opcodes: 2
Avg passing latency = 5 cyclesExample 4 — Corner Case: Empty Array, sort on Queue, product Overflow
module tb_method_corners;
int arr [];
int q [$];
initial begin
// ── Empty array: locators return empty queue ──────────────────
arr = new[0];
int r[$] = arr.find() with (item > 0);
$display("find on empty: size=%0d", r.size()); // 0
$display("sum on empty: %0d", arr.sum()); // 0
// min/max on empty: returns empty queue — guard [0] access!
int m[$] = arr.min();
if (m.size() > 0) $display("min = %0d", m[0]);
else $display("min: array empty"); // prints this
// ── sort() works on queues too ────────────────────────────────
q = '{5, 1, 4, 2, 3};
q.sort();
$display("sorted queue: %p", q); // '{1,2,3,4,5}
// ── sum with int overflow ────────────────────────────────────
// sum() returns same type as element — for 'int' that's 32-bit signed
// Large arrays can overflow. Cast to longint for large sums:
arr = new[4]('{2000000000, 2000000000, 1, 1});
int sum_int = arr.sum(); // OVERFLOW: wraps at 32-bit
longint sum_long = arr.sum() with (longint'(item)); // CORRECT: widened
$display("sum int = %0d", sum_int); // wrong (overflowed)
$display("sum longint= %0d", sum_long); // 4000000002
// ── unique preserves first-occurrence order ──────────────────
arr = '{3, 1, 2, 1, 3, 4};
int u[$] = arr.unique();
$display("unique order: %p", u); // '{3,1,2,4} — first occurrence order
$finish;
end
endmoduleSimulation Behavior — What to Know Before Using These
Return Type Rules
Locator methods always return a queue, even when you know logically only one result is possible. min() returns a 1-element queue, not a scalar. find_first() returns a 0-or-1-element queue. Always access the result via [0] and check size before accessing on methods that might return empty queues.
sort() Is Stable in Most Simulators
When two elements compare equal in a sort(), most simulator implementations preserve their relative order — this is called a stable sort. The LRM does not mandate stability, so you should not rely on it for correctness in portable code. If relative order of equal elements matters, add a secondary sort key using a more complex with expression: arr.sort() with ({item.priority, item.id}).
| Method | On empty array | Return value |
|---|---|---|
sort(), rsort() | No-op, no error | void |
sum(), product() | Returns 0 (neutral element) | 0 |
find(), find_index() | Returns empty queue | '{} |
min(), max() | Returns empty queue — guard [0] access | '{} |
unique() | Returns empty queue | '{} |
Where Array Methods Save the Most Code in Verification
// ── 1. SCOREBOARD: check all-pass with one expression ─────────────
bit all_passed = (results.sum() with (item.passed) == results.size());
if (!all_passed) $error("Not all transactions passed");
// ── 2. COVERAGE: verify all expected opcodes were exercised ────────
logic [7:0] seen_ops[];
logic [7:0] expected_ops [] = '{8'h10, 8'h20, 8'h30};
// ... capture seen_ops during simulation ...
logic [7:0] unique_seen[$] = seen_ops.unique();
if (unique_seen.size() < expected_ops.size())
$error("Coverage gap: only %0d/%0d opcodes exercised",
unique_seen.size(), expected_ops.size());
// ── 3. LATENCY ANALYSIS: find SLA violations ──────────────────────
parameter int SLA_CYCLES = 10;
result_t violations[$] = results.find() with (item.latency > SLA_CYCLES);
int violation_pct = violations.size() * 100 / results.size();
$display("SLA violations: %0d%% of transactions", violation_pct);
// ── 4. SORT TRANSACTIONS FOR DETERMINISTIC REPORT ─────────────────
// Sort by opcode first, then by transaction ID within same opcode
results.sort() with ({item.opcode, item.txn_id}); // compound sort key
// ── 5. FIND DUPLICATE TRANSACTION IDS (should never happen) ───────
int all_ids [$] = results.find_index() with (1); // all indices (always true)
int uniq_ids [$] = results.unique_index() with (item.txn_id);
if (all_ids.size() != uniq_ids.size())
$error("Duplicate TIDs detected!");
// ── 6. FIND FIRST ERROR AND STOP FURTHER CHECKING ─────────────────
result_t first_fail[$] = results.find_first() with (!item.passed);
if (first_fail.size() > 0)
$display("First failure: tid=%0d op=0x%02h",
first_fail[0].txn_id, first_fail[0].opcode);Bugs Engineers Hit With Array Methods
Bug 1 — Treating min()/max() Result as Scalar
int arr [] = '{30, 10, 50, 20};
// BUGGY: min() returns a QUEUE, not a scalar
int m = arr.min(); // type mismatch — may compile but gives wrong value
$display("m = %0d", m); // undefined behavior — NOT necessarily 10
// FIXED: index into the returned queue
int min_q[$] = arr.min();
if (min_q.size() > 0)
$display("min = %0d", min_q[0]); // 10
// Or in one expression (safe only when array is guaranteed non-empty):
$display("min = %0d", arr.min()[0]); // 10 — clean if array never emptyBug 2 — sort() Modifies Original — Don't Sort What You Still Need
int log [] = '{50, 10, 30}; // original arrival order matters
// BUGGY: sort() is in-place — arrival order is lost!
log.sort();
$display("first arrived: %0d", log[0]); // 10 — WRONG: was 50
// CORRECT: work on a COPY for analysis; preserve original
int sorted [] = log; // deep copy
sorted.sort(); // sort the copy — original log unchanged
$display("first arrived: %0d", log[0]); // 50 — correct
$display("sorted min: %0d", sorted[0]); // 10 — correctBug 3 — sum() Overflow With Large Values
int counters [] = '{1500000000, 1500000000, 500000000};
// BUGGY: sum() returns same type as element = int (32-bit signed)
// 1.5B + 1.5B = 3B which overflows int32 (max ~2.1B)
int total = counters.sum();
$display("total = %0d", total); // wrong — overflowed to negative
// FIXED: cast each element to a wider type in the with clause
longint safe_total = counters.sum() with (longint'(item));
$display("total = %0d", safe_total); // 3500000000 — correctBug 4 — find() Returns Elements, find_index() Returns Indices
int arr [] = '{50, 10, 40, 30};
// find() returns VALUES matching the condition
int vals[$] = arr.find() with (item > 30);
$display("values > 30: %p", vals); // '{50, 40} — the values themselves
// find_index() returns INDICES of matching elements
int idxs[$] = arr.find_index() with (item > 30);
$display("indices > 30: %p", idxs); // '{0, 2} — indices 0 and 2
$display("arr[%0d] = %0d", idxs[0], arr[idxs[0]]); // arr[0] = 50
// BUGGY: using find() when you actually need the index
int wrong_idx = vals[0]; // vals[0]=50 — this is a VALUE, not an index!
int val_at_wrong = arr[50]; // FATAL: index 50 out of bounds on 4-element arrayThe Two Result-Type Traps
Both families of method return something narrower than people expect, and both mistakes compile cleanly.
Locator methods return a queue, never a scalar. arr.min() gives a queue
containing one element, not an int. int m = arr.min(); is a type mismatch;
int m = arr.min()[0]; is the form — and it needs an empty-array guard, because
the queue form exists precisely so that an empty array can return an empty queue
rather than an undefined value.
A reduction's result type follows its with expression. This is the sharper
trap. arr.sum() with (item > 30) looks like it counts matching elements, and the
comparison yields a 1-bit value, so the sum is computed in one bit and wraps:
two matches give 0, three give 1. The count is not merely wrong, it is
approximately random with respect to the answer you wanted.
The fix is to widen the expression explicitly:
int n_big = arr.sum() with (int'(item > 30)); // ✓ 32-bit accumulationThe same reasoning covers the plain form. arr.sum() on a byte array
accumulates in 8 bits; widen with arr.sum() with (int'(item)) when the total can
exceed the element type.
module array_method_types_proof;
int arr [] = '{10, 25, 40, 40, 50, 5};
byte small [] = '{100, 100, 100};
int fails = 0;
task automatic expect (input string what, input bit cond);
if (cond) $display(" PASS %s", what);
else begin $display(" FAIL %s", what); fails++; end
endtask
initial begin
$display("\n1. Locator methods return QUEUES");
begin
int mn [$] = arr.min();
int fnd[$] = arr.find() with (item > 30);
int idx[$] = arr.find_index() with (item == 40);
$display(" arr.min() -> queue of %0d, value %0d", mn.size(), mn[0]);
$display(" find(>30) -> queue of %0d", fnd.size());
$display(" find_index(40) -> indices %p", idx);
expect("min() returns a 1-element queue", mn.size() == 1);
expect("find() returns every match", fnd.size() == 3);
expect("find_index() returns positions", idx == '{2, 3});
end
$display("\n2. An empty array returns an EMPTY queue, not an error");
begin
int empty [];
int mn [$] = empty.min();
expect("min() of an empty array is an empty queue", mn.size() == 0);
// mn[0] here would be an out-of-bounds read. Guard before indexing.
end
$display("\n3. The 1-bit sum trap");
begin
// The with expression is a comparison, so it is 1 bit wide, and the
// sum accumulates in 1 bit. Three matches wrap to 1.
bit bad = arr.sum() with (item > 30);
int good = arr.sum() with (int'(item > 30));
$display(" sum with (item > 30) = %0d <- wrapped", bad);
$display(" sum with (int'(item > 30)) = %0d <- correct", good);
expect("uncast boolean sum wraps in 1 bit", bad == 1);
expect("int'-cast sum gives the real count", good == 3);
end
$display("\n4. The same rule for the plain form");
begin
byte bad_total = small.sum(); // 300 does not fit in a byte
int ok_total = small.sum() with (int'(item));
$display(" byte sum = %0d (wrapped), int sum = %0d", bad_total, ok_total);
expect("byte accumulation wraps", bad_total != 300);
expect("widened accumulation is right", ok_total == 300);
end
$display("\n%0s (%0d failures)\n",
fails == 0 ? "ALL CHECKS PASSED" : "CHECKS FAILED", fails);
if (fails) $fatal(1, "array_method_types_proof failed");
$finish;
end
endmoduleA coverage gate passed on a run that exercised two opcodes out of sixteen
ONE-BIT-REDUCTION-SUMAn end-of-test check counted how many distinct opcodes a random test had exercised and failed the run if the count fell below a threshold. It never failed. Reviewing coverage separately showed runs that had exercised as few as two opcodes out of sixteen sailing through a gate that required at least eight.
The gate had been in place for four months and had passed on every run, which is what made it credible and what made nobody look at it.
int seen_count [16]; // per-opcode hit counts, filled by the monitor
function void check_opcode_coverage();
// Intent: count how many opcodes were hit at least once.
bit n_hit = seen_count.sum() with (item > 0); // ✗ 1-bit accumulation
if (n_hit < 8)
`uvm_error("COV", $sformatf("only %0d opcodes exercised", n_hit))
endfunctionPrinting the value the gate was comparing settled it immediately:
opcodes actually hit : 2
n_hit (as computed) : 0
gate condition : (0 < 8) -> TRUE ... but no error was reported?The second surprise explained the first. n_hit is declared bit, so
n_hit < 8 compares a 1-bit value against 8 — and the comparison is performed
at the width of the larger operand, giving 0 < 8, which is true. So the error
should have fired.
It did not, because on most runs n_hit computed to 1 rather than 0: with an odd
number of opcodes hit, the 1-bit sum wraps to 1, and 1 < 8 is also true. Adding
the print showed the gate firing on some runs and not others, at which point the
real behaviour became clear — the value being tested had no relationship to the
quantity it was supposed to represent. It was the parity of the opcode count.
The result type of a reduction with a with clause follows the type of the
expression. item > 0 is a comparison, so it is one bit wide, and sum()
accumulates in one bit — which is addition modulo 2. The result is the parity
of the number of matching elements, not the count.
Two coincidences kept it hidden. Declaring the target as bit meant no
truncation warning at the assignment, because nothing was being truncated — the
expression really was 1 bit. And a parity value of 0 or 1 is always less than 8,
so the gate's condition was in fact satisfied on every run; what varied was
whether the surrounding logic treated the result as meaningful.
The deeper issue is that this gate belongs to the class of checks that can only pass. It compared a quantity that was never the count against a threshold, and no stimulus could have made it report correctly. Four months of green results were not evidence of coverage; they were evidence that the check was inert.
function void check_opcode_coverage();
// Widen the with expression so the accumulation happens in 32 bits.
int n_hit = seen_count.sum() with (int'(item > 0));
if (n_hit < 8)
`uvm_error("COV", $sformatf("only %0d of %0d opcodes exercised",
n_hit, seen_count.size()))
endfunctionVerifying the fix takes a self-check rather than another run of the gate: build a known array with a known number of matches and assert the count. Three matches must return 3, not 1 — which is the check in the runnable proof above, and it fails against the old expression.
Two habits follow.
Cast the with expression whenever it is a comparison or a narrow field.
int'(item > x) for counting, int'(item.field) for summing a narrow struct
member. The cast costs nothing and removes the entire class.
Give any threshold check a positive control. A gate that has never failed is indistinguishable from a gate that cannot fail, and the cheapest way to tell them apart is to feed it a value that must trip it — once, deliberately, as part of bring-up. The same reasoning appears in assertion-based verification, where a property that never fires and a property that cannot fail produce identical reports.
For the width and sign rules the accumulation follows, see 4-state versus 2-state types; for the queues these locator methods return, see queues and dynamic arrays.
Interview Questions
A queue containing one element, not a scalar. int m = arr.min(); is a type mismatch; the form is int m = arr.min()[0];.
The queue is not an accident of the API. It exists so that an empty array has something meaningful to return — an empty queue — rather than an undefined scalar. That is also why indexing straight into the result is unsafe: arr.min()[0] on an empty array is an out-of-bounds read, so guard with if (arr.size() > 0) or capture the queue and check its size first.
The whole locator family behaves this way: min, max, find, find_first, find_last, find_index, unique, unique_index all return queues. find_first() returning a queue of at most one element surprises people who expect a scalar, and it is the same reasoning — the "no match" case needs a representation.
find() returns a queue of the matching elements; find_index() returns a queue of their positions.
Which you want depends on what you do next. If you only need the values — summing them, printing them, checking how many there are — find() is direct. If you need to modify the original array, or to correlate with a parallel array, you need the indices, because the elements returned by find() are copies and writing to them does not touch the original.
The _first and _last variants of both exist and still return queues, of at most one element. arr.find_first_index() with (item > 30) is the usual way to ask "where is the first element above the threshold", and it returns an empty queue rather than a sentinel value when nothing matches — which is safer than returning -1 and much easier to get wrong if you forget to check size().
Use arr.sum() with (int'(item > threshold)). The int' cast is not optional.
The obvious form, arr.sum() with (item > threshold), is wrong in a way that compiles silently. The result type of a reduction follows the type of its with expression, and a comparison is one bit wide — so the sum accumulates modulo 2 and returns the parity of the match count rather than the count. Two matches give 0, three give 1.
Nothing warns, because nothing is being truncated: the expression genuinely is 1 bit. If the result is assigned to a bit, there is not even a width mismatch to notice.
Casting the expression to int makes the accumulation 32-bit and the answer correct. The same rule applies to the plain form: byte_arr.sum() accumulates in 8 bits, so widen with sum() with (int'(item)) whenever the total can exceed the element type.
The alternative, arr.find(...).size(), is correct and allocates an intermediate queue — fine for a one-off check, wasteful in a hot loop.
By one field: arr.sort() with (item.score); for ascending, arr.rsort() with (item.score); for descending. Both operate in place and return nothing — a common mistake is writing sorted = arr.sort(), which does not compile.
For a compound key, concatenate the fields into a single packed value so they sort lexicographically: arr.sort() with ({item.priority, item.id});. Priority is the more significant field, so it dominates, and id breaks ties within equal priorities. The fields must be packed types for the concatenation to be meaningful, and the field order in the braces is the sort precedence.
Two cautions. The sort is not specified to be stable, so equal keys may not retain their original relative order — if that matters, include a tiebreaker in the key rather than relying on it. And because these methods modify the original, sorting an array you also need in arrival order requires copying it first.
Reduce both sides to sorted unique sets and compare them directly.
int seen_u [$] = seen_ops.unique(); // queue of distinct values
int exp_u [$] = expected_ops;
seen_u.sort();
exp_u.sort();
if (seen_u != exp_u)
`uvm_error("COV", $sformatf("opcode set mismatch: saw %p, expected %p",
seen_u, exp_u))Note that there is no auto in SystemVerilog — the result of unique() is a queue and must be declared as one. Queue comparison with != is element-wise, so this single check catches both a missing opcode and an unexpected one, which is what "exactly the expected set" requires.
If you want the two failures reported separately — usually more useful, because a missing opcode is a stimulus gap and an unexpected one is a decode bug — take the differences explicitly:
int missing [$] = exp_u.find() with (!(item inside {seen_u}));
int extra [$] = seen_u.find() with (!(item inside {exp_u}));The inside operator accepts a queue as its set, which keeps this readable without a nested loop.
Effectively none of them. The locator and reduction methods are testbench constructs: they return queues, which are dynamically sized, and dynamic sizing has no hardware meaning.
That is not a limitation so much as a category distinction. arr.find() with (...) asks a question whose answer size is not known until run time, and hardware has to be built for a fixed maximum. The synthesisable equivalent of a find is a comparator tree with a fixed width and an explicit "how many results can I hold" decision — which is exactly the decision the method hides.
Where they earn their place is everything around the DUT: scoreboards filtering transactions, coverage models counting distinct values, monitors sorting observed items, end-of-test checks comparing sets. In that context they replace loops that are easy to write wrongly, and the code reads as the intent rather than the mechanism.
The one habit to carry across the boundary is the type discipline. The same int'() cast that fixes a reduction in a testbench is the same width reasoning that governs an RTL accumulator — the language just warns you about it less in the testbench.
Where This Is Specified
- IEEE 1800-2023 §7.12 — Array manipulation methods. The three families and their return types: locator methods return a queue of results, ordering methods operate in place, and reduction methods return a single value.
- IEEE 1800-2023 §7.12.1 — Array locator methods.
find,find_index,find_first,find_last,min,max,unique,unique_index, and the rule that they return an empty queue when nothing matches. - IEEE 1800-2023 §7.12.3 — Array reduction methods.
sum,product,and,or,xor, and the rule that the result type follows thewithexpression — the basis of the 1-bit accumulation trap above. - IEEE 1800-2023 §7.12.2 — Array ordering methods.
sort,rsort,reverse,shuffle, their in-place semantics, and thewithclause used to supply a sort key. - IEEE 1800-2023 §7.12.4 — Iterator index querying. The
itemandindexnames available inside awithclause. - IEEE 1800-2023 §11.4.13 —
insideoperator. Set membership against an array or queue, used by the set-difference form above.
Best Practices and Coding Guidelines
Guard min()/max() on empty arrays
These return empty queues on empty arrays. Always check .size() > 0 before accessing [0], or validate the array is non-empty before calling.
Copy before sort if order matters
sort() and rsort() modify in-place. If you need the original order for any subsequent operation, copy first: auto copy = arr; copy.sort();
Cast to longint for large sums
For int arrays with large values: arr.sum() with (longint'(item)). The result type of sum() defaults to the element type — 32-bit int overflows silently.
Use sum() for counting, find() for values
Counting matches: arr.sum() with (condition) — no queue allocation. Getting values: arr.find() with (condition) — returns the actual elements. Use the right tool for the task.
| Task | One-liner | Avoid |
|---|---|---|
| Find minimum value | arr.min()[0] (guard for empty) | arr.min() assigned to scalar |
| Count matching elements | arr.sum() with (item > x) | arr.find() with (...).size() — allocates queue |
| Sort struct by field desc | arr.rsort() with (item.field) | Manual bubble sort loop |
| Get unique values | arr.unique() | Manual dedup loop with associative array |
| Check all-pass | arr.sum() with (item.ok) == arr.size() | foreach loop with flag variable |
| First failure | arr.find_first() with (!item.ok) | Loop with break — more code, same result |
Summary — Chapter 3 Complete
Array methods turn verification boilerplate into readable one-liners. The with clause is the key that unlocks them for real-world use — sort by any field, filter by any condition, sum any sub-expression. The three rules that prevent the most bugs: locator methods return queues (index with [0]), ordering methods modify in-place (copy first if you need the original), and sum() inherits the element type (cast to longint for large values).
- Three families: ordering (in-place), locator (return queue), reduction (return scalar). Know which family each method belongs to before using it.
with (item)is a lambda.item= current element. Use it to operate on fields, compute expressions, or define filter conditions.- All locator methods return queues — even when only one result is possible. Always use
[0]to get the scalar value, and guard against empty results. sort()is in-place. If you need the original order after sorting, work on a copy.sum() with (condition)counts matches. More efficient thanfind().size()for counting-only tasks.
Part of SystemVerilog Fundamentals·Arrays·Lesson 18 of 53
View program