Part 3 · Constraint Randomization · Intermediate

unique Constraints

unique { arr } semantics, mixing scalars and arrays, generating shuffled permutations, unique vs randc, and what uniqueness costs the solver.

unique: pairwise inequality in one keyword

The unique constraint declares that every listed value is pairwise distinct . unique { arr }; means no two elements of arr are equal — semantically identical to writing arr[i] != arr[j] for every i ≠ j pair, but vastly more readable and easier for the solver to recognize as an all-different problem. The braces can mix scalars and arrays: every scalar and every element of every listed array must differ from all the others.

systemverilog
class distinct_ids;
  rand bit [3:0] id[6];
  rand bit [3:0] master_id;
  rand bit [3:0] a, b, c;

  constraint c {
    unique { id };                 // 6 array elements all distinct
    unique { master_id, id };      // master differs from every element too
    unique { a, b, c };            // three scalars pairwise distinct
  }
endclass

// unique { id } on bit [3:0] id[6]:
//   16 possible values, 6 slots -> satisfiable.
// If id were bit [2:0] id[10]:
//   8 values, 10 slots -> PIGEONHOLE: unsatisfiable, randomize()=0.

The pigeonhole check is the first thing to verify: the element type must offer at least as many values as there are entries in the unique set, after intersecting with any range constraints. unique combined with foreach (id[i]) id[i] inside {[0:5]} on a 6-element array leaves exactly one solution family: a permutation of 0..5.


Generating a shuffled permutation

That last observation is the standard interview recipe for a random permutation of 0..N-1 : fix the size to N, restrict every element into [0, N-1], and require uniqueness. With N values squeezed into N distinct slots, the only legal assignments are permutations — and a conforming solver picks uniformly among them.

systemverilog
class perm_gen #(int N = 8);
  rand int unsigned p[];

  constraint c {
    p.size() == N;
    foreach (p[i]) p[i] inside {[0:N-1]};
    unique { p };
  }
endclass

module top;
  initial begin
    perm_gen #(8) g = new();
    repeat (3) begin
      void'(g.randomize());
      $display("%p", g.p);   // e.g. '{3,0,6,1,7,4,2,5}
    end
  end
endmodule

// Procedural alternative (no solver): arr.shuffle()
//   - cheaper, but NOT constraint-aware: you cannot say
//     "permutation where p[0] != 0" with shuffle alone.
//   - the constraint version composes with any extra rule:
constraint c_extra { p[0] != 0; }   // derangement-at-0 permutation

Mention shuffle() as the cheap procedural alternative when no additional constraints apply — knowing when not to use the solver is part of the expected answer.


unique vs randc

Both produce non-repeating values, but on different axes. unique enforces distinctness within one randomize() call, across the listed variables/elements . randc enforces non-repetition across successive randomize() calls of one variable — it cycles through a permutation of its value space over time. They answer different questions and are not interchangeable.

diagram
UNIQUE vs RANDC — TWO AXES OF NON-REPETITION

                 one call, many values     many calls, one value
                 (spatial)                 (temporal)
                 ----------------------    ----------------------
  keyword        unique { arr }            randc bit [3:0] v;
  guarantees     all listed values         v cycles all 16 values
                 differ in THIS solve      before any repeat
  scope          across variables/         across successive
                 elements                  randomize() calls
  resets when    every call re-solves      cycle exhausted ->
                                           new random permutation

  call 1:  unique arr = {3,7,1}   randc v = 5
  call 2:  unique arr = {7,7? NO  randc v = 2   (not 5)
           -> always distinct}    ...
  call 16:                        randc v = last unseen value
  call 17:                        new permutation begins
systemverilog
class compare;
  rand  bit [3:0] arr[4];
  randc bit [3:0] chan;

  constraint c { unique { arr }; }
  // arr: each CALL gives 4 distinct values; values may repeat
  //      between calls (call1 {1,5,9,2}, call2 {5,3,1,8} is fine).
  // chan: one value per call; guaranteed to visit all 16 values
  //       across 16 calls before any repeat.
endclass

Solver cost of uniqueness

unique over N elements implies on the order of N²/2 pairwise inequalities. Modern solvers special-case all-different sets, but cost still grows with N and with how tightly other constraints squeeze the value space — a unique set that exactly fills its range (the permutation case) is the hardest version, since almost every partial assignment constrains the rest. Keep unique sets as small as the requirement allows, and widen value ranges when you can.


Interview angle

What interviewers probe

  • "Generate a random permutation of 0..N-1" — expected: size==N, inside [0:N-1], unique; bonus for naming shuffle() as the unconstrained alternative.

  • "unique vs randc?" — expected: unique is within-one-call across elements; randc is across-calls for one variable cycling its space.

  • "What if the range is smaller than the array?" — expected: pigeonhole, unsatisfiable, randomize() returns 0 — check value-count vs slot-count first.

  • "What does unique cost?" — expected: O(N²) pairwise inequalities conceptually; tight ranges make it harder; permutation is the worst case.

A subtle senior-level point: randc cannot participate in dist or solve...before, so when an interviewer asks for "non-repeating but weighted" values, the answer is unique-style constraints plus history kept in the class, not randc.

Key takeaways

  • unique { ... } = pairwise distinct across all listed scalars and array elements, within one solve.

  • Permutation recipe: size == N, elements inside [0:N-1], unique — only permutations remain.

  • unique is spatial (one call), randc is temporal (across calls) — different tools for different requirements.

  • Check the pigeonhole condition before anything else: value-space size must be >= unique-set size.

  • Uniqueness cost grows roughly quadratically and worsens as ranges tighten — keep sets small.

Common pitfalls

  • unique set larger than the available value space — silent unsatisfiability, randomize() returns 0.

  • Expecting unique to prevent repeats BETWEEN randomize() calls — that is randc territory (or manual history).

  • Using randc when distribution weighting is also needed — randc is illegal in dist; restructure with rand + constraints.

  • Reaching for the solver to shuffle an unconstrained array — arr.shuffle() is free; save the solver for constrained permutations.

  • Forgetting that range constraints shrink the value space the pigeonhole check applies to — unique passes alone, fails once inside [a:b] is added.