Declaring rand byte data[] makes every element random. The size is random only when a constraint names data.size(): an unconstrained dynamic array keeps its current size (IEEE 1800 18.4), so an empty one stays empty.
Inside a constraint, arr.size() is an ordinary random expression. It can be bounded, tied to another rand field, or pinned with inside.
Declaration | Size random | Elements random | Rule |
|---|---|---|---|
| Only if | Yes | Constrain and bound |
| Only if | Yes | Same rule as dynamic arrays |
| No, part of the type | Yes | Constrain values only |
risky has rand bit [7:0] payload[] and no constraint at all. A fresh object is randomized. What is payload.size() afterwards? And after payload = new[8] and another call?0, then 8. Without a size constraint the solver never resizes the array; it only randomizes the elements that exist.
The bench below runs both classes. risky shows the size staying where it was; safe bounds the payload to 4 to 16 bytes and ties a queue's length to burst_len.
class risky;
rand bit [7:0] payload[]; // no size constraint
endclass
class safe;
rand bit [7:0] payload[];
rand int lens[$];
rand bit [3:0] burst_len;
constraint c_size {
payload.size() inside {[4:16]}; // hard bounds
lens.size() == burst_len; // size tied to another rand field
burst_len inside {[1:8]}; // and still bounded
}
endclass
module tb_sizes;
risky r = new();
safe s = new();
int seen[int];
initial begin
if (!r.randomize()) $fatal(1, "FAIL: risky solve failed");
$display("risky, fresh: size %0d after randomize()", r.payload.size());
if (r.payload.size() != 0) $fatal(1, "FAIL: an unconstrained array was resized");
r.payload = new[8]; // sized by hand
if (!r.randomize()) $fatal(1, "FAIL: risky solve failed");
$display("risky, new[8] first: size %0d after randomize()", r.payload.size());
if (r.payload.size() != 8) $fatal(1, "FAIL: a hand-sized array changed size");
repeat (500) begin
if (!s.randomize()) $fatal(1, "FAIL: safe solve failed");
if (s.payload.size() < 4 || s.payload.size() > 16) $fatal(1, "FAIL: size %0d out of bounds", s.payload.size());
if (s.lens.size() != s.burst_len) $fatal(1, "FAIL: queue size %0d, burst_len %0d", s.lens.size(), s.burst_len);
seen[s.payload.size()]++;
end
$display("safe: %0d different payload sizes seen in 500 calls", seen.num());
if (seen.num() != 13) $fatal(1, "FAIL: some legal size never appeared");
$display("PASS: no size constraint keeps the size; a bounded size covers 4 to 16 and the queue follows burst_len");
$finish;
end
endmodulerisky, fresh: size 0 after randomize()
risky, new[8] first: size 8 after randomize()
safe: 13 different payload sizes seen in 500 calls
PASS: no size constraint keeps the size; a bounded size covers 4 to 16 and the queue follows burst_lenlens.size() == burst_len mean to the solver?One more equation over two random integers. The solver picks the length and the queue size together, and every result has them equal.
Element rules cannot be listed until the size is known. IEEE 1800 18.5.8.1 fixes the order: size constraints are solved first, then the foreach constraints, with size() read as a constant inside them.
That order can make randomize() fail. If the size picked first leaves no legal element values, the call returns 0, even though another size would have worked.
Under the standard, a size of 8, 9 or 10 picked first fails, so roughly 3 calls in 10. Verilator looked ahead: 0 of 300 failed, and it only ever chose sizes 1 to 7.
class coupled;
rand bit [3:0] data[]; // 4-bit elements: at most 15
constraint c {
data.size() inside {[1:10]};
foreach (data[i]) data[i] == data.size() * 2; // sizes 8, 9 and 10 have no legal element
}
endclass
module tb_coupled;
coupled c = new();
int sizes[int];
int unsat = 0;
initial begin
repeat (300) begin
if (!c.randomize()) begin unsat++; continue; end
foreach (c.data[i])
if (c.data[i] != c.data.size() * 2) $fatal(1, "FAIL: data[%0d] = %0d at size %0d", i, c.data[i], c.data.size());
sizes[c.data.size()]++;
end
$display("unsatisfied calls: %0d of 300", unsat);
begin
int lo, hi;
void'(sizes.first(lo));
void'(sizes.last(hi));
$display("sizes seen: %0d (smallest %0d, largest %0d)", sizes.num(), lo, hi);
end
if (sizes.exists(8) || sizes.exists(9) || sizes.exists(10)) $fatal(1, "FAIL: an infeasible size came back");
$display("PASS: every solved array obeys data[i] == 2 * size, and only sizes 1 to 7 came back");
$finish;
end
endmoduleunsatisfied calls: 0 of 300
sizes seen: 7 (smallest 1, largest 7)
PASS: every solved array obeys data[i] == 2 * size, and only sizes 1 to 7 came backVerilator is kinder than the standard here, and a commercial solver may not be. Do not rely on either: bound the size so that every size in range has legal elements, here data.size() inside {[1:7]}.
No element value works, because twice 9 does not fit in 4 bits, so randomize() returns 0. Bound the size to 1 to 7 to make every pick feasible.
An unconstrained rand array is not resized, so a freshly built one stays empty. A size constraint with no upper bound is the opposite risk: one seed can ask for a huge array. Give every random size both bounds.
With a size constraint, the array keeps the solver's size after randomize() returns, and the next call solves the size again. Without one, a manual new[N] survives every call, which is the usual way to fix a size by hand.
pre_randomize sized an array with new[8], and there is no size constraint. What size follows randomize()?8. Without a size constraint the array is not resized; only its elements are randomized.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator prints a harmless WIDTHTRUNC warning for if (!obj.randomize()), because randomize() returns an int, and an UNSATCONSTR warning when a solve fails on purpose. Icarus 13 does not support constraints.
Verilator 5.048 solved 300 of 300 calls of the coupled class, using only sizes 1 to 7; the standard's size-first order would allow failures.