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.

systemverilog
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
endmodule

Note 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.

diagram
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.
systemverilog
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.

systemverilog
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
endclass

Resizing 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.