Part 3 · Constraint Randomization · Intermediate

The Solution-Space Mental Model

Constraints as a set of legal assignments, uniform solver choice per the LRM, why procedural-assignment thinking fails, and 2-variable solution-space diagrams.

The model: a set of legal tuples

Take every rand variable in scope of a randomize() call and form the tuple (v1, v2, ..., vN). The full space of tuples is the cross product of each variable's type range. Each active constraint removes tuples that violate it. What remains after all constraints are applied is the solution space . The LRM's intent is that randomize() selects uniformly among the remaining tuples (absent dist, solve...before, or randc, which re-weight or stage the choice). If the space is empty, randomize() returns 0 and the variables keep their previous values.

systemverilog
class two_var;
  rand bit [2:0] x;   // 0..7
  rand bit [2:0] y;   // 0..7

  constraint c {
    x + y <= 4;       // removes every tuple with x+y > 4
    x != y;           // removes the diagonal
  }
endclass
// Full space: 8 x 8 = 64 tuples.
// After x+y<=4 : 15 tuples remain.
// After x!=y   : 12 tuples remain (drops (0,0),(1,1),(2,2)).
// randomize() picks one of the 12, each with probability 1/12.
diagram
SOLUTION SPACE FOR  x+y<=4  AND  x!=y   (x right, y up)

  y
  4 | *  .  .  .  .  .  .  .        * = legal tuple (12 total)
  3 | *  *  .  .  .  .  .  .        . = removed by constraints
  2 | *  *  X  .  .  .  .  .        X = removed by x!=y only
  1 | *  X  *  *  .  .  .  .
  0 | X  *  *  *  *  .  .  .
    +------------------------
      0  1  2  3  4  5  6  7  x

  randomize() = throw a dart uniformly at the * cells.
  P(x=0) = 4/12,  P(x=4) = 1/12  -> NOT uniform per variable,
  even though the choice among TUPLES is uniform.

That last line is the first non-obvious consequence: uniform over tuples does not mean uniform per variable . A variable whose value appears in many legal tuples is more likely than one squeezed into few. Most "the solver is biased!" complaints are exactly this effect, working as specified.


Why procedural thinking fails

Engineers coming from RTL or software instinctively read constraints top-to-bottom as assignments with conditions. That model gives wrong predictions immediately. Constraint order within a block is irrelevant { x < y; y < 10; } and { y < 10; x < y; } describe the identical solution set. There is no "x is chosen first". All constraints from all active blocks (including inherited ones and inline with clauses) are conjoined into one system and solved simultaneously.

systemverilog
class order_irrelevant;
  rand bit [3:0] x, y;

  // These two blocks produce EXACTLY the same solution space:
  constraint c1 { x < y;  y < 10; }
  // constraint c1_alt { y < 10;  x < y; }   // identical set

  // Procedural reading FAILS here:
  //   "x is random, then y must beat x" suggests x uniform 0..15.
  //   Reality: tuples (x,y) with x<y<10 are uniform; large x values
  //   appear in FEW tuples, so P(x=8) << P(x=0).
endclass

module check;
  initial begin
    order_irrelevant o = new();
    int hist[16];
    repeat (10000) begin
      void'(o.randomize());
      hist[o.x]++;
    end
    // hist[0] ~ 9/45 of hits, hist[8] ~ 1/45 — triangular, not flat.
  end
endmodule

The histogram experiment is worth actually running once: with x < y < 10 there are 45 legal tuples, 9 of which have x==0 and only 1 of which has x==8 — the per-variable distribution is triangular even though no dist appears anywhere.


What counts as "the constraint set"

The solver conjoins more than the obvious blocks. For one randomize() call, the constraint system includes:

  • All active constraint blocks of the object, including those inherited from base classes (a derived constraint with the same name overrides the base version).

  • Constraints of nested rand objects — fields of contained objects join the same joint solve.

  • The inline with clause of this call: obj.randomize() with { addr < 100; } adds temporary constraints.

  • State variables (non-rand) frozen at their pre-call values — they parametrize the space but are never changed.

  • Implicit legality: each variable's declared type range; randc cycling state.

systemverilog
class base_txn;
  rand bit [7:0] len;
  constraint c_len { len inside {[1:64]}; }
endclass

class small_txn extends base_txn;
  constraint c_len { len inside {[1:8]}; }   // OVERRIDES base c_len
endclass

module m;
  initial begin
    small_txn t = new();
    // joint set: overridden c_len + inline with-clause
    void'(t.randomize() with { len != 4; });
    // solution space: {1,2,3,5,6,7,8} — 7 tuples, uniform pick
  end
endmodule

Knowing the override rule matters here: if the derived class had named its constraint differently (c_small), both blocks would apply and the space would be their intersection — same result in this example, but very different when constraints conflict.


Interview angle

What interviewers probe

  • "With x < y, is x uniformly distributed?" — expected: no; uniformity is over TUPLES, so x values appearing in more tuples are more likely. Sketch the triangle.

  • "Does constraint order in a block matter?" — expected: no; constraints are a conjoined declarative system, not sequential statements.

  • "What happens when no solution exists?" — expected: randomize() returns 0, variables retain previous values, no exception — which is why return codes must be checked.

  • "What joins the constraint set besides the class's own blocks?" — expected: inherited blocks (with name-override rule), nested rand objects, inline with, frozen state variables.

The fastest way to demonstrate mastery is to draw the 2-D grid for whatever toy constraint the interviewer gives, mark legal cells, and read probabilities off the picture. It converts every follow-up question into counting.

Key takeaways

  • A constraint set defines a solution space of tuples; randomize() picks one, uniformly by default.

  • Uniform over tuples ≠ uniform per variable — tuple counts per value determine marginal probabilities.

  • Constraint order inside blocks is irrelevant; all active constraints conjoin into one simultaneous system.

  • The system includes inherited blocks (name-override rule), nested rand objects, inline with, and frozen state vars.

  • Empty space => randomize() returns 0 and silently keeps old values — always check the return.

Common pitfalls

  • Reading constraints procedurally and predicting per-variable uniformity — the x<y triangle disproves it.

  • Ignoring randomize() return values — failure keeps stale values and tests pass on garbage.

  • Forgetting inherited constraints still apply (unless name-overridden) — the space is tighter than the derived class suggests.

  • Assuming inline with REPLACES class constraints — it intersects with them; conflicts make the space empty.

  • Expecting the solver to modify state (non-rand) variables to satisfy constraints — they are frozen inputs.