Part 3 · Constraint Randomization · Intermediate

Q&A: Arrays & Nested Objects

Constraining dynamic array size and elements, object allocation before randomize, the sum() overflow trap, uniqueness without unique, and 2D arrays.

Q: How do you constrain a dynamic array's size AND its elements?

Direct answer: constrain arr.size() like any expression, and use foreach for the elements; the solver picks a size and populates elements consistently in one solve — there is no separate “resize phase”, and foreach automatically ranges over whatever size was chosen. Element constraints may even reference the size or the index.

systemverilog
class pkt;
  rand bit [7:0] data[];

  constraint c_size { data.size() inside {[4:16]}; }
  constraint c_elem { foreach (data[i]) data[i] inside {[1:200]}; }

  // Index- and size-aware element constraints:
  constraint c_shape {
    foreach (data[i]) {
      if (i == 0)               data[i] == 8'hA5;            // header marker
      if (i == data.size() - 1) data[i] == 8'h5A;            // trailer marker
    }
  }
endclass

Follow-up you should expect

“What if you don’t constrain size at all?” — The size itself is then solver-chosen with tool-specific defaults (often biased small, possibly zero), so portable code always bounds it. And “can one element constrain another?” — yes, foreach bodies can reference data[i-1] with an index guard; that is how ordering and run-length structures are built.

Junior vs senior answer

  • Junior: “constrain arr.size() and use foreach.” — names both pieces.

  • Senior: adds single-solve semantics (no resize phase), index/size-aware element constraints, the unbounded-size portability hazard, and cross-element references with guards.


Q: Why must nested objects exist before randomize() is called?

Direct answer: because randomize() solves — it never allocates. A null handle has no fields to include in the constraint problem: the LRM-conformant behavior is to skip null rand handles, so the nested object’s constraints silently vanish from the solve (some tools warn or error instead). The rule: construct in new() (or build_phase), randomize afterwards — allocation is the test writer’s job, solving is the solver’s.

systemverilog
class config_c;
  rand bit [3:0] mode;
  constraint c_mode { mode inside {[1:8]}; }
endclass

class env_txn;
  rand config_c cfg;

  function new();
    cfg = new();   // without this: cfg stays null, c_mode never solved,
  endfunction      // and cfg.mode is a null deref when the driver reads it
endclass

Follow-up you should expect

“Does randomize() re-randomize the nested object’s fields every call?” — Yes, if the handle is rand and non-null: parent and child fields form one joint problem, so cross-boundary constraints (parent referencing cfg.mode) are solved simultaneously, not in parent-then-child order. If you want the child held constant, cfg.rand_mode(0) on the handle excludes the whole object from the solve while its values stay readable as constants.

Junior vs senior answer

  • Junior: “you’d get a null pointer error.” — plausible, but the dangerous case is the silent skip.

  • Senior: explains skip-not-allocate semantics, the silently-vanishing-constraints consequence, joint parent/child solving, and rand_mode(0) on the handle as the freeze knob.


Q: What is the sum() overflow trap?

Direct answer: array reduction sum() accumulates in the element type’s width . For bit [7:0] arr[10], the addition is modulo 256 — so arr.sum() == 100 is satisfied by true sums of 100, 356, 612… The solver will happily pick wrapped solutions, and your “sums to 100” array arrives summing to 356. Fix: widen each item inside the reduction with a with clause — arr.sum() with (int'(item)) == 100.

systemverilog
class budget;
  rand bit [7:0] arr[10];

  // TRAP: 8-bit accumulator — wrapped totals "pass"
  // constraint c_bad { arr.sum() == 100; }

  // FIX: 32-bit accumulation via per-item cast
  constraint c_sum  { arr.sum() with (int'(item)) == 100; }
  constraint c_elem { foreach (arr[i]) arr[i] inside {[1:50]}; }
endclass

Follow-up you should expect

“Where else does the same width trap bite?” — Everywhere constraint arithmetic exceeds operand width: a + b <= 200 on bytes, size * count <= 4096 on 32-bit factors, address-end checks start + len at the space boundary. One habit fixes all of them: widen at the operands , because casting the result happens after the wrap already occurred.

Junior vs senior answer

  • Junior: knows sum() “has an overflow issue”, fuzzy on mechanism or fix.

  • Senior: states accumulation-in-element-width precisely, writes the with (int'(item)) fix from memory, and generalizes to the whole family of width-wrap constraint bugs.


Q: Make all array elements unique WITHOUT the unique keyword

Direct answer: pairwise inequality over index pairs — nested foreach with an i < j guard so each pair is constrained once. This is both the portability answer (pre-2012 tools) and the comprehension check behind the unique keyword: an interviewer who suspects you only know the keyword asks for this form.

systemverilog
class uniq;
  rand bit [7:0] arr[8];

  constraint c_pairwise {
    foreach (arr[i])
      foreach (arr[j])
        if (i < j) arr[i] != arr[j];
  }
  // 8 elements → 28 pairwise inequalities; solver satisfies all at once.
  // Feasibility: need ≥ 8 distinct candidates — 8-bit type has 256. Fine.
endclass

Follow-up you should expect

“How does this scale?” — Quadratically: N(N-1)/2 constraints; at hundreds of elements solve time grows noticeably, where unique {arr} lets the solver use a dedicated all-different algorithm. And the recurring trap follow-up: “would randc work instead?” — no; randc cycles one variable across successive calls and says nothing about distinct elements within one call.

Junior vs senior answer

  • Junior: writes adjacent-only arr[i] != arr[i+1], or proposes randc.

  • Senior: pairwise with the i<j guard, counts the constraints, notes the feasibility precondition and the quadratic-vs-native-unique scaling.


Q: How do you randomize a 2D array with constraints?

Direct answer: declare it rand and use multi-index foreach (m[i][j]) — wait, the correct foreach spelling for multiple dimensions is foreach (m[i, j]), comma-separated in ONE bracket; that spelling itself is interview bait. Row/column aggregate constraints (sums, uniqueness per row) are expressed by fixing one index and iterating the other.

systemverilog
class matrix;
  rand bit [7:0] m[4][4];

  // Per-cell bounds — note the comma form for 2 dims
  constraint c_cell { foreach (m[i, j]) m[i, j] inside {[0:9]}; }

  // Each row sums to 20 (widened accumulation, manual inner sum)
  constraint c_row {
    foreach (m[i, j])
      if (j == 0)
        (int'(m[i][0]) + int'(m[i][1]) + int'(m[i][2]) + int'(m[i][3])) == 20;
  }

  // Diagonal dominance example: diagonal cell strictly largest in its row
  constraint c_diag {
    foreach (m[i, j])
      if (i != j) m[i][j] < m[i][i];
  }
endclass

Follow-up you should expect

“Why the manual four-term sum instead of m[i].sum()?” — Reduction methods on a slice of a multidimensional rand array inside constraints have uneven tool support; the explicit sum is the portable whiteboard answer for small fixed dimensions, and for dynamic dimensions you restructure as an array of row objects, each owning a 1D array with its own sum constraint — which also re-uses everything you know about nested rand objects.

Junior vs senior answer

  • Junior: writes foreach (m[i][j]) — the bracket form that doesn’t parse — and is surprised.

  • Senior: knows the comma spelling cold, the guarded-inner-index idiom for row aggregates, and the rows-as-objects restructuring for anything dynamic.

Key takeaways

  • size() and foreach solve together in one pass — bound the size, guard cross-element references.

  • randomize() never allocates — null rand handles are silently skipped, constraints and all.

  • sum() accumulates in element width — with (int'(item)) every time; widen operands, not results.

  • Pairwise i<j inequality is the no-keyword uniqueness answer; know its quadratic cost.

  • Multi-dim foreach is comma-form: foreach (m[i, j]) — the bracket form is bait.

Common pitfalls

  • Unbounded dynamic array size — tool-dependent defaults, occasionally zero-length surprises.

  • Constructing nested objects after randomize instead of before — constraints silently skipped.

  • Casting the sum result instead of the items — the accumulator already wrapped.

  • Reduction methods on multi-dim slices in constraints — portability minefield; restructure instead.