Part 3 · Constraint Randomization · Intermediate
Constraining Array Size
rand dynamic arrays and queues, .size() in constraints, solver handling of size vs elements, and the unconstrained-size memory trap.
What rand means for an array
Declaring rand byte data[] makes both the size and every element random. On randomize() the solver picks a size, allocates the array to that size, and assigns each element a value satisfying all element constraints. The same applies to queues (rand byte q[$]) — the solver picks the number of entries. Fixed-size arrays (rand byte fixed[8]) only randomize elements; the size is part of the type.
The key constraint primitive is arr.size() used inside a constraint block. It behaves like any other random expression: you can bound it, relate it to other rand variables, or pin it with inside sets.
class packet;
rand bit [7:0] data[]; // dynamic array: size AND elements random
rand bit [7:0] hdr[4]; // fixed array: only elements random
rand int lens[$]; // queue: entry count AND entries random
rand bit [3:0] burst_len;
constraint c_size {
data.size() inside {[4:64]}; // hard bounds — ALWAYS do this
lens.size() == burst_len; // size tied to another rand var
lens.size() inside {[1:8]}; // and still bounded
}
endclass
module top;
initial begin
packet p = new();
repeat (3) begin
void'(p.randomize());
$display("data.size=%0d lens.size=%0d burst_len=%0d",
p.data.size(), p.lens.size(), p.burst_len);
end
end
endmoduleNote that lens.size() == burst_len is bidirectional: the solver can pick burst_len to fit a size choice or vice versa. Size is just another integer variable in the constraint set.
How the solver treats size vs elements
Conceptually the size must be known before element constraints can be enumerated — you cannot talk about data[7] until the array has at least 8 entries. The LRM resolves this by requiring the solver to treat .size() and the elements as one simultaneous constraint problem : the chosen size must permit a legal assignment of every element, and element constraints implicitly constrain the viable sizes. In practice most implementations solve size first internally, then elements — but you must not write code that depends on a sequential model, because constraints that couple size and element values are still solved jointly.
SOLVER VIEW OF A DYNAMIC ARRAY
constraint set:
data.size() inside {[2:4]};
foreach (data[i]) data[i] > data.size();
candidate solutions (size, elements):
size=2 : each element in (2..255] -> legal
size=3 : each element in (3..255] -> legal
size=4 : each element in (4..255] -> legal
+-----------------------------------------------+
| size and elements form ONE solution space. |
| The solver picks (size, e0..eN-1) tuples that |
| jointly satisfy every constraint. |
+-----------------------------------------------+
WRONG mental model: "size is randomized, THEN
elements are randomized against the fixed size,
and a size that breaks elements causes failure."
A conforming solver simply never picks that size.class coupled;
rand bit [7:0] data[];
constraint c {
data.size() inside {[1:10]};
// element constraint that depends on size — solved jointly:
foreach (data[i]) data[i] == data.size() * 2;
}
endclass
// Every solution has all elements equal to twice the chosen size.
// randomize() never fails here: every size in [1:10] admits a
// legal element assignment, so the joint space is non-empty.If a size choice would make the element constraints unsatisfiable, a conforming solver excludes that size from the solution space rather than failing. randomize() returns 0 only when no (size, elements) combination is legal.
The unconstrained-size trap
If you declare rand byte data[] and never constrain data.size(), the size can legally be anything representable — and simulators differ wildly. Some default to small sizes, some keep the pre-randomize size, and some can pick enormous values that allocate gigabytes and kill the simulation. The LRM does not mandate a friendly default. Always bound the size explicitly , even when "any size is fine" — write data.size() inside {[0:256]} and move on.
class risky;
rand bit [7:0] payload[];
// NO size constraint — vendor-dependent behavior:
// - may allocate a huge array (memory blowup / sim hang)
// - may keep old size (stale-size surprises across calls)
endclass
class safe;
rand bit [7:0] payload[];
constraint c_size { payload.size() inside {[0:256]}; }
// Optional: shrink to a known size before solving when a test
// wants reproducible small payloads regardless of constraints
function void pre_randomize();
// pre_randomize runs before solving; useful for setup,
// but the SIZE BOUND above is what actually protects you
endfunction
endclassResizing across randomize calls
A dynamic array keeps its solver-chosen size after randomize() returns. The next call re-solves the size from scratch (subject to constraints) — previous contents and size are discarded for rand arrays. If you manually sized an array with new[N] hoping to pin the size, that does not survive: only a constraint like data.size() == N pins it reliably.
Interview angle
What interviewers probe
"What happens if you randomize a dynamic array without a size constraint?" — expected answer: size is part of the random state, behavior is implementation-defined, risk of huge allocations; always bound it.
"Does the solver pick size first, then elements?" — expected answer: conceptually joint; size and elements are one solution space, and a size that breaks element constraints is simply never chosen.
"How do you make the payload length equal a header field?" — expected answer: data.size() == len inside one constraint, noting it is bidirectional.
"Difference between rand byte a[] and rand byte a[8]?" — dynamic randomizes size + elements; fixed randomizes elements only.
A strong answer always mentions the joint solution space — saying "size is randomized first" without qualification is the classic intermediate-level miss, because it implies a size choice could later "fail" element constraints, which a conforming solver never allows.
Key takeaways
rand dynamic arrays and queues randomize size AND elements; fixed arrays randomize elements only.
arr.size() is an ordinary constraint expression — bound it, equate it to other rand fields, anything.
Size and elements are one joint solution space; sizes that break element constraints are excluded, not failed.
Always bound size explicitly — unconstrained size is vendor-defined and can allocate huge arrays.
Common pitfalls
No size constraint on a rand dynamic array — memory blowup or vendor-dependent sizes in regression.
Pre-sizing with new[N] and expecting randomize() to keep that size — only a size() constraint pins it.
Assuming size is solved strictly before elements and writing constraints that rely on sequential semantics.
Constraining data.size() but forgetting queues — rand q[$] needs the same treatment.
Equating size to an unconstrained 32-bit int (size() == n with n unbounded) — n can go huge; bound both.