Part 3 · Constraint Randomization · Intermediate

Arrays of rand Objects

Construct-then-randomize for object arrays, constraints across elements, and modeling transaction sequences with gap and back-to-back constraints.

The solver randomizes fields, never allocates handles

Declaring rand txn seq[] makes the array of handles random in size, and recursively randomizes the rand fields of every non-null object the handles point to. What randomize() will never do is call new(): a null handle stays null and its "fields" simply do not exist in the constraint set. The mandatory pattern is therefore construct first, randomize second — size the array, new() every element, then call randomize() once on the container.

systemverilog
class txn;
  rand bit [31:0] addr;
  rand bit [7:0]  len;
  rand bit        is_write;
  constraint c_len { len inside {[1:16]}; }
endclass

class burst;
  rand txn seq[];

  // Size choice is made BEFORE solving, in pre_randomize,
  // because we must allocate objects before the solver runs.
  function void pre_randomize();
    int n = $urandom_range(4, 8);
    seq = new[n];
    foreach (seq[i]) seq[i] = new();
  endfunction
endclass

module top;
  initial begin
    burst b = new();
    void'(b.randomize());   // solves every seq[i].addr/len/is_write
    foreach (b.seq[i])
      $display("[%0d] %s addr=%h len=%0d",
               i, b.seq[i].is_write ? "WR" : "RD",
               b.seq[i].addr, b.seq[i].len);
  end
endmodule

Note the trade-off: because objects must exist before solving, the number of objects cannot be a solver decision the way a scalar array's size can. Pick the count procedurally (pre_randomize or test code), or over-allocate and use a rand "valid count" field that constraints reference.


Constraints across object array elements

Once the objects exist, the container's constraint block can reference seq[i].field inside foreach — the solver flattens all objects' rand fields into one constraint problem, so cross-object relations (ascending addresses, alternating direction, no two consecutive writes) are just element constraints over fields.

systemverilog
class ordered_burst;
  rand txn seq[];

  function void pre_randomize();
    seq = new[6];
    foreach (seq[i]) seq[i] = new();
  endfunction

  constraint c_relations {
    // ascending, non-overlapping address windows
    foreach (seq[i])
      if (i > 0)
        seq[i].addr >= seq[i-1].addr + (seq[i-1].len * 4);

    // first transaction is always a write
    seq[0].is_write == 1;

    // no two consecutive reads
    foreach (seq[i])
      if (i > 0)
        !(seq[i].is_write == 0 && seq[i-1].is_write == 0);
  }
endclass

Everything solves in one call : each object's own constraints (like c_len in txn) and the container's cross-object constraints form a single joint problem. This is what distinguishes one-call container randomization from a loop of per-object seq[i].randomize() calls — the loop cannot enforce relations that look forward (e.g. "sum of all lens == 64"), only backward-looking ones against already-solved items.

diagram
ONE-CALL vs PER-ELEMENT RANDOMIZATION

  container.randomize()             foreach loop of seq[i].randomize()
  -------------------------         ---------------------------------
  all objects' fields in ONE        each object solved ALONE,
  joint constraint set              earlier results frozen as state

  can enforce:                      can enforce:
   - sum of lens == 64               - seq[i] vs seq[i-1] (backward,
   - global ordering                   passed via constraint args or
   - "exactly 2 writes"                state variables)
                                    cannot enforce:
                                     - totals over future elements
                                     - global counts / budgets

  cost: one big solve               cost: N small solves (often faster,
        (can be slow for big N)           weaker expressiveness)

Modeling transaction sequences: gaps and back-to-back

A common stimulus model adds a rand inter-transaction gap (idle cycles before each item) to the transaction or the container. Constraining gaps lets one knob sweep traffic density: all-zero gaps give back-to-back stress, bounded gaps give throttled traffic, and a mostly-zero dist gives bursty realistic patterns.

systemverilog
class spaced_burst;
  rand txn       seq[];
  rand bit [7:0] gap[];      // idle cycles BEFORE seq[i]

  function void pre_randomize();
    seq = new[8];
    foreach (seq[i]) seq[i] = new();
  endfunction

  constraint c_shape {
    gap.size() == seq.size();
    gap[0] == 0;                          // start immediately

    // bursty profile: mostly back-to-back, occasional long gap
    foreach (gap[i])
      if (i > 0)
        gap[i] dist { 0 := 70, [1:3] := 25, [10:20] := 5 };

    // at least one TRUE back-to-back pair somewhere
    gap.sum() with (int'(item.index > 0 && item == 0)) >= 1;
  }
endclass

// driver consumes:  repeat (gap[i]) @(posedge clk);  drive(seq[i]);

The counting-sum at the end reuses the sum-of-booleans idiom from the aggregates lesson to guarantee the interesting corner (a real back-to-back pair) appears in every generated burst rather than hoping the distribution produces one.


Interview angle

What interviewers probe

  • "You declared rand txn seq[10] and randomize() — what happens?" — expected: null-handle awareness; nothing is randomized (or a runtime error on access) until every element is new()'d. Construct-then-randomize.

  • "Why can't the solver choose how many objects to create?" — expected: randomize() never calls new(); object count is procedural. Mention the over-allocate + rand valid-count workaround.

  • "Constrain transaction N against transaction N-1" — expected: container-level foreach over seq[i].field with an i>0 guard.

  • "One randomize on the container vs a loop of per-item randomizes?" — expected: joint solve enables global/forward constraints (sums, counts); the loop is cheaper but only supports backward-looking relations.

The construct-before-randomize rule is one of the most common real-bug interview questions because the failure mode is silent in some tools: randomize() succeeds, the handles stay null, and the testbench crashes later at first dereference.

Key takeaways

  • randomize() recursively solves rand fields of non-null objects — it never constructs; new() every element first.

  • Object count is a procedural decision (pre_randomize or test code), not a solver decision.

  • Container-level foreach over seq[i].field expresses cross-object relations, solved jointly with each object's own constraints.

  • One-call container randomization supports global budgets and forward-looking relations; per-element loops do not.

  • A rand gap[] array beside the transaction array is the standard knob for back-to-back vs throttled vs bursty traffic.

Common pitfalls

  • Calling randomize() on an array of null handles — silently nothing solved, null-deref later; always new() in pre_randomize or before.

  • Allocating objects with seq = new[N] but forgetting the per-element seq[i] = new() loop — new[N] creates null handles only.

  • Expecting seq.size() to be solver-chosen like a scalar array — object existence precedes solving; pick count procedurally.

  • Re-using one object for every slot (seq[i] = shared_h) — all slots alias one object; each needs its own new().

  • Enforcing a global budget (total len) with a per-element randomize loop — impossible; switch to one-call container randomization.