Part 3 · Constraint Randomization · Intermediate

std::randomize() & Scope Randomization

Randomizing local variables, with-clauses on std::randomize, when scope randomization beats class randomization, and $urandom comparison.

Randomization without a class

std::randomize(vars...) invokes the same constraint solver as class randomize(), but on local or module-scope variables instead of class fields. The listed variables play the role of rand fields; an optional with { ... } clause supplies the constraints. The return-value contract is identical: 1 with all variables updated to a simultaneous legal solution, or 0 with nothing changed.

systemverilog
task drive_idle_gap();
  int unsigned gap;
  bit [3:0]    burst;

  // Solve gap and burst TOGETHER under shared constraints
  if (!std::randomize(gap, burst) with {
        gap inside {[1:100]};
        burst inside {[1:8]};
        gap > burst * 10;          // relationship between the two
      })
    $error("std::randomize failed");

  repeat (gap) @(posedge clk);
  send_burst(burst);
endtask

What the solver does: exactly the class-randomize pipeline — intersect the three with-clause expressions into a (gap, burst) solution space and pick uniformly. Because gap > burst*10 ties them, the solve is joint: the solver does not pick gap then burst; it picks a legal PAIR. This is the key advantage over calling $urandom_range twice, which cannot express cross-variable relationships.

Scope variables in the with-clause

systemverilog
task drive_after(int unsigned min_gap);   // task arg = state variable
  int unsigned gap;
  // with-clause sees surrounding scope: min_gap is read as a constant
  assert (std::randomize(gap) with { gap inside {[min_gap : min_gap+50]}; });
endtask

Variables not listed in the argument list but referenced in the with-clause are state variables — read, never written — mirroring how class constraints read non-rand fields.


When scope randomize beats class randomize

Class randomization is the right tool for transactions — reusable objects with intrinsic legality rules that travel with the data. Scope randomization wins when the random decision is local and ephemeral: delays, loop counts, one-off mode picks inside a test or task, where defining a class would be ceremony with no reuse.

diagram
WHICH RANDOMIZATION TOOL?

  need cross-variable constraints?
        │
   no   │   yes
        │    └────────────────────────┐
        ▼                             ▼
  single bounded value?      values belong to a reusable
        │                    transaction with intrinsic rules?
   yes  │  no                      │
        ▼                     no   │   yes
  $urandom_range(hi,lo)            │    └──► CLASS randomize()
  (fast, no solver,                ▼         rand fields + constraints
   always succeeds)         std::randomize(...) with {...}
                            (solver power, no class ceremony)

  rule of thumb:
  • transaction content         class randomize
  • local sequencing decisions  std::randomize with
  • simple independent draws    $urandom / $urandom_range
systemverilog
// Typical test-level mix of all three:
task run_traffic(bus_txn t);
  int n_txns, mode;

  // joint local decision → std::randomize
  assert (std::randomize(n_txns, mode) with {
            n_txns inside {[10:50]};
            mode   inside {[0:2]};
            (mode == 2) -> n_txns > 30;    // stress mode needs volume
          });

  repeat (n_txns) begin
    assert (t.randomize());                 // transaction → class solve
    drive(t);
    repeat ($urandom_range(0, 5))           // simple gap → $urandom_range
      @(posedge clk);
  end
endtask

What each call does: the std::randomize jointly picks count and mode honoring the implication; t.randomize() runs the transaction's own constraint set; $urandom_range does a plain bounded draw with no solver involvement at all — three tools, each at the right weight class.


$urandom and $urandom_range vs randomize

$urandom returns a 32-bit unsigned random value; $urandom_range(maxval, minval) bounds it inclusively (and tolerates swapped arguments). They are thread-stable system functions, not solver calls: no constraints, no return-value protocol, no failure mode. Two properties matter for choosing them: they are dramatically cheaper than a solver invocation, and they draw from the calling THREAD's random stream (random stability — covered in the seeding lesson), whereas class randomize() draws from the OBJECT's stream.

systemverilog
bit [31:0] raw, lo32;
int unsigned d6;

initial begin
  raw  = $urandom;                  // full 32-bit draw
  d6   = $urandom_range(6, 1);      // inclusive [1:6]
  d6   = $urandom_range(1, 6);      // same — args may be swapped
  raw  = $urandom(42);              // optional seed arg: seeds THIS
                                    // process's stream, then draws

  // What $urandom CANNOT do:
  //   pick (a,b) with a+b==100        → needs std::randomize with
  //   honor class legality rules      → needs class randomize
  //   cyclic no-repeat values         → needs randc
end

Comparison table

diagram
$urandom_range   std::randomize     class randomize
  constraints        none             with-clause        class + inline with
  cross-variable     no               yes                yes
  failure possible   no               yes (returns 0)    yes (returns 0)
  cost               trivial          solver call        solver call
  random stream      calling thread   calling thread     the object
  reuse of rules     none             none (inline)      high (in the class)
  typical use        gaps, delays     local joint picks  transactions

Interview angle

This topic shows up as a judgment question — “which randomization mechanism would you use for X, and why?” — and as a semantics check on the differences.

  • “Difference between $urandom and $random?” — $urandom is unsigned, thread-stable, SystemVerilog; $random is the old signed Verilog function with weaker stability guarantees. Use $urandom.

  • “Can std::randomize fail?” — yes, contradictory with-clause returns 0 and changes nothing; check it exactly like class randomize.

  • “How do you randomize two locals so their sum is fixed?” — std::randomize(a,b) with { a+b == N; } — $urandom cannot express the relationship.

  • “Why prefer class constraints over a with-clause on std::randomize for a packet?” — legality rules belong WITH the data type: reusable, overridable by inheritance, checkable via randomize(null).

  • “Does $urandom_range(1,6) work, or must max come first?” — both orders work; the function takes max,min but swaps if needed.

Key takeaways

  • std::randomize() is the full constraint solver applied to scope variables; the with-clause holds the constraints and the return value must be checked.

  • Use class randomize for transactions, std::randomize for local joint decisions, $urandom_range for simple independent draws.

  • Unlisted variables referenced in a with-clause are state variables — read, never written, like non-rand class fields.

  • $urandom/$urandom_range never fail and never solve constraints — cheap thread-stable draws, wrong tool for related variables.

Common pitfalls

  • Ignoring the std::randomize return value — same silent-stale-data failure as class randomize.

  • Building cross-variable relationships out of sequential $urandom_range calls and rejection loops — slow and distribution-skewed; one std::randomize does it right.

  • Putting transaction legality rules into scattered with-clauses instead of the class — rules drift apart across tests.

  • Using $random in new code — signed result and weaker stability cause subtle reproducibility differences across tools.

  • Assuming $urandom participates in constraint solving — it is a plain draw; constraints never see it.