Part 3 · Constraint Randomization · Intermediate

rand vs randc

Uniform resampling vs cyclic permutation, randc period behavior, width and solver limits, and the all-values-once interview question.

Two different sampling contracts

Both qualifiers mark a class field as solver-controlled, but they make different statistical promises. rand means sample uniformly from the legal values, independently each call — repeats are normal and expected (8 values can repeat on the very next call with probability 1/8). randc means random-cyclic : the solver conceptually shuffles all legal values into a random permutation, deals them out one per randomize() call, and only reshuffles after the permutation is exhausted. Within one cycle, no value repeats.

systemverilog
class demo;
  rand  bit [1:0] r;   // 4 legal values, independent uniform picks
  randc bit [1:0] c;   // 4 legal values, dealt as a random permutation
endclass

module top;
  demo d = new();
  initial begin
    repeat (8) begin
      void'(d.randomize());
      $display("r=%0d  c=%0d", d.r, d.c);
    end
  end
endmodule

// Possible output:
//   r=2  c=1      r: any value any time (repeats fine)
//   r=2  c=3      c: first cycle is some permutation of {0,1,2,3}
//   r=0  c=0
//   r=3  c=2      ◄── cycle 1 complete: 1,3,0,2
//   r=3  c=2      c reshuffles: new permutation begins
//   r=1  c=0
//   r=0  c=3
//   r=2  c=1      ◄── cycle 2 complete: 2,0,3,1

What the solver does: for r, each call is an independent uniform draw — the solver intersects constraints into a legal set and picks any member. For c, the solver maintains per-variable cycle state: it remembers which values of the current permutation are already used and excludes them from the next pick. That extra bookkeeping is exactly why randc has width limits and constraint restrictions.


The randc period, drawn out

diagram
randc bit [1:0] c;        4 legal values  period = 4

  call#:   1    2    3    4  │  5    6    7    8  │  9 ...
           ┌────────────────┐│ ┌────────────────┐│
  value:   1    3    0    2  │  2    0    3    1  │  ...
           └── permutation A┘│ └── permutation B┘│
                             │                    │
                       reshuffle            reshuffle
                       (new random          (new random
                        permutation)         permutation)

  Guarantees WITHIN a cycle:        Guarantees ACROSS cycles:
  • every value exactly once        • NONE — last value of cycle A
  • no repeats                        may equal first value of cycle B
                                      (e.g. calls 4 and 5 above: 2, 2)

The period equals the number of legal values, not the number of representable values — if a constraint restricts a 4-bit randc field to 5 legal values, the cycle length is 5. Note the boundary subtlety in the diagram: back-to-back duplicates CAN occur exactly at a cycle boundary, because the new permutation is independent of the old one.

Width and solver limits

Cyclic bookkeeping must track which values were used, so implementations cap randc width — the LRM requires support for at least 8 bits, and typical simulators allow somewhere between 16 and 32 bits before erroring out. Beyond that, tracking 2^32 used-values is impractical. Two more hard rules: randc variables are solved before rand variables (an implicit solve-before), and a randc variable cannot be the target of dist — a distribution weighting contradicts the exactly-once-per-cycle contract.

systemverilog
class limits;
  randc bit [31:0] big_id;     // many tools: error or unsupported
  randc bit [7:0]  small_id;   // fine everywhere (LRM minimum)
  rand  bit [7:0]  data;

  // ILLEGAL: dist on randc — weighting contradicts cyclic contract
  // constraint c_bad { small_id dist { 0 := 9, [1:255] :/ 1 }; }

  // Legal: inside restricts the cycle to 5 values → period 5
  constraint c_set { small_id inside {10, 20, 30, 40, 50}; }
endclass

What the solver does with c_set: the permutation is built only over {10, 20, 30, 40, 50}, so each group of five consecutive randomize() calls yields those five values in some random order. Because randc is solved first, the constraint set involving small_id is fixed before data is picked — if a constraint ties data to small_id, data's legal range is computed from the already-chosen small_id value.


When randc misleads

The cyclic guarantee is per object, per variable, across consecutive randomize() calls of that object . Three situations quietly break the all-values-once expectation people reach for randc to get:

  • Re-randomizing for OTHER fields still advances the randc cycle — every randomize() call consumes one permutation slot, even if you only cared about re-rolling a different field.

  • Two different objects each have independent cycle state — ten packet objects with randc id do NOT collectively cover 0..255; each object covers it separately.

  • Constraints that change between calls invalidate the simple period story — if the legal set shrinks mid-cycle, the solver must reconcile used-value tracking with the new set (tool-dependent corner).

systemverilog
class pkt;
  randc bit [1:0] id;     // intent: use each id once before repeating
  rand  bit [7:0] data;
endclass

pkt p = new();
initial begin
  // Intent: 4 packets, ids 0..3 each exactly once. Looks right...
  repeat (4) begin
    void'(p.randomize());            // advances id cycle: OK so far
    if (p.data == 0)
      void'(p.randomize());          // re-roll for data — but this ALSO
                                     // consumes an id slot! The cycle
                                     // advances and one id is "burned"
    send(p);
  end
  // Result: sent ids may be e.g. 1,0,2,1 — a value repeated and a
  // value missing, despite randc. The cycle itself never repeated;
  // we just discarded one of its elements.
end

What the solver does: it has no idea you discarded a result. Each randomize() deals the next permutation element; the re-roll consumed id=3 (say) and you never sent it. The fix is to re-roll only the offending field — p.randomize(data) with a field list (covered in the randomize() lesson) leaves id untouched, or constrain data so re-rolls are unnecessary.


Interview angle

The canonical question: “Generate all values of an N-bit field exactly once, in random order.” The expected first answer is randc:

systemverilog
class walker;
  randc bit [3:0] val;   // 16 calls → 16 distinct values, random order
endclass

walker w = new();
initial begin
  repeat (16) begin
    void'(w.randomize());
    $display("%0d", w.val);   // permutation of 0..15
  end
end

Strong candidates then volunteer the limits before being asked: (1) it only works within one object's consecutive calls — any extra randomize() burns a slot; (2) width is implementation-limited, so for a 32-bit space you instead shuffle an explicit list; (3) the cycle reshuffles after exhaustion, so call 17 may repeat call 16's value. The fallback they want to hear for wide fields:

systemverilog
// Wide-field alternative: explicit shuffle, no randc width limit
int unsigned vals[];
vals = new[1024];
foreach (vals[i]) vals[i] = i;
vals.shuffle();                    // random permutation, exactly once each
foreach (vals[i]) use_value(vals[i]);

Rapid follow-ups to be ready for

  • “Can two consecutive randc values be equal?” — Yes, exactly at a cycle boundary; never within a cycle.

  • “Can you put dist on a randc variable?” — No; weighting contradicts the once-per-cycle contract, it is a compile error.

  • “Are randc variables solved with or after rand variables?” — Before; randc has an implicit solve-before over rand.

  • “Do two objects share a randc cycle?” — No; cycle state is per variable per object.

Key takeaways

  • rand = independent uniform draw each call; randc = random permutation dealt one value per call, reshuffled when exhausted.

  • randc period = number of LEGAL values under current constraints, and duplicates can occur only across a cycle boundary.

  • randc variables are solved before rand variables and cannot take dist weights; width is implementation-limited (LRM minimum 8 bits).

  • Every randomize() call advances every randc cycle in that object — re-rolling for another field burns permutation slots.

Common pitfalls

  • Using randc on wide fields (>16–32 bits) — tools error out or silently degrade; use an explicit shuffle() instead.

  • Expecting all-values-once across multiple objects — cycle state is per object, not global.

  • Re-randomizing an object in a loop and assuming the randc field still covers everything sent — discarded calls burn cycle slots.

  • Putting dist on a randc field — illegal, and conceptually contradictory.

  • Assuming call N+1 differs from call N — at a cycle boundary the same value can appear twice in a row.