Part 3 · Constraint Randomization · Intermediate

foreach Constraints

Per-element constraints, index-dependent relations like ascending arrays, multi-dimensional foreach, and iteration over queues and associative arrays.

foreach is constraint replication, not a loop

Inside a constraint block, foreach (arr[i]) expr; does not "execute" anything. It replicates the constraint expression once per element , with i substituted, and hands the whole expanded set to the solver. For a 4-element array, foreach (a[i]) a[i] < 10; is exactly the constraint set { a[0]<10, a[1]<10, a[2]<10, a[3]<10 }. Because the array size may itself be random, the replication count is tied to the size variable — another reason size and elements solve jointly.

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

  constraint c_size { data.size() inside {[4:16]}; }

  constraint c_elem {
    // replicated once per element:
    foreach (data[i]) data[i] inside {[8'h20:8'h7E]};  // printable ASCII

    // index-dependent: even indexes low, odd indexes high
    foreach (data[i])
      if (i % 2 == 0) data[i] < 8'h40;
      else            data[i] >= 8'h40;
  }
endclass

The loop index i is a compile-time iteration variable, not a rand variable. You can use it in arithmetic (data[i] == i * 4), comparisons, and conditionals — the solver sees only the fully expanded per-element constraints.


Index-dependent relations: the ascending-array pattern

Relating neighboring elements is the most-asked foreach pattern. The trap is the array boundary: data[i+1] does not exist when i is the last index. Guard the relation so it only applies where both elements exist.

systemverilog
class sorted_arr;
  rand int unsigned a[];
  constraint c_size { a.size() inside {[5:10]}; }

  // Strictly ascending — two equivalent guard styles:
  constraint c_up_guard_high {
    foreach (a[i])
      if (i < a.size() - 1)
        a[i] < a[i+1];
  }

  // Alternative: guard on the LOW side, reference a[i-1]
  constraint c_up_guard_low {
    foreach (a[i])
      if (i > 0)
        a[i] > a[i-1];
  }
endclass

Both styles are correct; pick one and stay consistent (here they coexist redundantly for illustration — in real code keep one). The i > 0 guard with a[i-1] is often preferred because a.size() - 1 mixes a random size into the guard expression, which some older tools handled poorly. Note that strict ascent over N elements also forces the value range to contain at least N distinct values — with bit [2:0] elements (8 values) and size 10, the constraint set is unsatisfiable and randomize() returns 0.

diagram
FOREACH EXPANSION FOR AN ASCENDING ARRAY (size=4)

  source:  foreach (a[i]) if (i > 0) a[i] > a[i-1];

  expands to the solver as:
    (i=0)  no constraint        (guard false)
    (i=1)  a[1] > a[0]
    (i=2)  a[2] > a[1]
    (i=3)  a[3] > a[2]

  chained relations:  a[0] < a[1] < a[2] < a[3]

  +-----------------------------------------------+
  | Each pairwise constraint is independent text, |
  | but the solver sees the conjunction — values  |
  | are forced into a strictly increasing chain.  |
  +-----------------------------------------------+

Multi-dimensional, queues, and associative arrays

foreach iterates every dimension you name. For a 2-D array, foreach (m[i][j]) visits all (row, column) pairs; foreach (m[i]) visits only rows, letting you constrain whole-row properties. Queues iterate like dynamic arrays. Associative arrays iterate over existing keys — randomize() never creates or removes keys, so foreach over an associative array constrains the values at keys that already exist.

systemverilog
class matrix;
  rand bit [7:0] m[4][4];           // fixed 4x4
  rand int       q[$];              // queue
  rand bit [7:0] cfg[string];       // associative — keys fixed by testbench

  constraint c {
    // all 16 cells, both indexes available:
    foreach (m[i][j]) m[i][j] inside {[0:99]};

    // diagonal dominance: diagonal cell biggest in its row
    foreach (m[i][j])
      if (i != j) m[i][i] > m[i][j];

    q.size() inside {[2:6]};
    foreach (q[k]) q[k] inside {[-100:100]};

    // associative: iterates existing string keys only
    foreach (cfg[s]) cfg[s] != 8'hFF;
  }
endclass

// Testbench seeds the associative keys BEFORE randomize():
//   c.cfg["mode"] = 0; c.cfg["speed"] = 0;
//   void'(c.randomize());   // values at "mode"/"speed" randomized

What foreach cannot do

  • It cannot change the iteration set — you cannot add queue entries or associative keys from inside a constraint.

  • The index variable is not rand — you cannot ask the solver to "pick an index"; instead make a separate rand index variable and constrain arr[idx] via element comparison idioms.

  • Function calls on elements inside foreach are restricted like any constraint expression — no side effects, treated as state at solve start.


Interview angle

What interviewers probe

  • "Write a constraint for a strictly increasing array" — they watch whether you guard the boundary index. Unguarded a[i] < a[i+1] is the #1 instant red flag.

  • "Is foreach a loop?" — expected: no, it is constraint replication; the solver receives the expanded conjunction, all solved simultaneously.

  • "Ascending 3-bit array of size 10 — what happens?" — expected: unsatisfiable, randomize() returns 0; strict ordering needs at least size-many distinct values.

  • "Constrain element at a random position" — expected: the index cannot be the foreach variable; use a separate rand idx plus foreach (a[i]) if (i == idx) ... or direct a[idx] reference.

Bonus points in interviews for mentioning that the replication count is bound to the random size() — it shows you understand the joint solving model rather than a procedural picture.

Key takeaways

  • foreach replicates a constraint per element — the solver sees the expanded conjunction, solved jointly with size.

  • Guard neighbor relations at array boundaries: if (i > 0) a[i] > a[i-1] is the canonical ascending pattern.

  • Strict ordering requires enough distinct values in the element type — otherwise unsatisfiable.

  • foreach on associative arrays visits existing keys only; randomize() never alters the key set.

Common pitfalls

  • Unguarded a[i+1] or a[i-1] references at array boundaries — out-of-range element in the expanded constraint.

  • Treating the foreach index as random — it is an iteration variable; use a separate rand index for "pick a position".

  • Strictly ascending constraint on a narrow element type with a larger size — silent randomize() failure.

  • Expecting foreach on an empty associative array to create keys — it iterates nothing.

  • Writing if/else element conditions that conflict with a separate blanket foreach range — over-constrained set, randomize() returns 0.