Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
    • Randomization Basics: rand, randc & randomize()
      • rand vs randc
      • The randomize() Call
      • Hooks and Nested Objects
      • Random Stability and Seeding
    • Constraint Operators & Expressions
    • Dynamic Constraint Control
    • Randomizing Arrays and Queues
    • How the Constraint Solver Works
    • Debugging Randomization Failures
    • Interview Drills — Constraints and Randomization Q&A
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Randomization Basics: rand, randc & randomize()
  1. Topics
  2. SystemVerilog
  3. Constraint Randomization — Interview-Critical Deep Dive
  4. Randomization Basics: rand, randc & randomize()
  5. The randomize() Call

Part 3 · Constraint Randomization · Intermediate

The randomize() Call

The randomize contract, return checks, check-only mode, field lists and tool choice.

On this page

    Why this matters

    The design received the same packet twice. A randomize() call had failed, nobody checked its return value, and the test sent the previous packet again.

    After this lesson you can: check every randomize() call, say what a failed solve does to the object, and pick class randomize, std::randomize or $urandom_range for a job.

    A solver call with a contract

    Every class implicitly gets a built-in randomize method. Its contract is precise: gather every active constraint, solve for every enabled rand field, then either commit a complete legal solution and return 1, or commit nothing and return 0.

    There is no partial success. The solve is atomic across all fields, which is why the return value is load-bearing.

    design.sv
    // snippet: the idiom, for any object t
    if (!t.randomize())
      $fatal(1, "randomize failed: constraints contradict");

    On success the solver found an addr and len pair satisfying all three constraint expressions at once. On failure the constraint set was unsatisfiable, and addr and len keep whatever values they held before the call.

    Verilator, for one, prints a WIDTHTRUNC warning for !t.randomize(), because randomize() returns an int; the warning is harmless.

    Some lint flows flag a discarded randomize return. The rule does not need the warning: check the return on every call regardless.

    Call

    Result

    What t holds

    1. t.randomize()

    Returns 1

    A new legal packet

    2. t.randomize()

    Returns 1

    Another new legal packet

    3. t.randomize() with {addr == 0;}, contradicting c_addr

    Returns 0, unchecked

    Still packet 2, so the test sends packet 2 again

    No error, no crash: the design simply receives a duplicate, and coverage counts a transaction that tested nothing new.

    What gets solved

    The solve set is every rand or randc field with rand mode on, recursing into any non-null rand class handle. Non-rand fields are state variables: the solver reads them but never writes them.

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // not-run: Verilator 5.048 cannot constrain size() of a queue inside a nested rand object (it treats it as state), so randomize() fails
    class payload;
      rand bit [7:0] bytes_q[$];
      constraint c_sz { bytes_q.size() inside {[1:16]}; }  // without it the queue is not resized
    endclass
    
    class frame;
      rand bit [15:0] id;        // solved
      bit  [3:0]      rev;       // NOT rand: read-only state for the solver
      rand payload    pl;        // non-null: the solver recurses into bytes_q
      payload         dbg;       // not rand: never touched, even if non-null
    
      constraint c_rev { id[15:12] == rev; }  // constraining rand BY state
    endclass
    
    module top;
      frame f = new();
      initial begin
        f.pl  = new();             // construct before randomize
        f.rev = 4'h3;
        assert (f.randomize());
        // solver wrote: f.id with id[15:12]==3, plus f.pl.bytes_q
        // solver read : f.rev
        // untouched   : f.dbg
      end
    endmodule

    With rev held at 3, the top nibble of id is forced to 3. The solver descends into pl because pl is rand and non-null, sizing and filling the queue in the same global solve. Without c_sz the queue would not be resized: IEEE 1800 18.4 says an array whose size is not constrained keeps its size, so an empty queue stays empty.

    dbg stays invisible to the solver regardless of its contents.

    Check-only mode and partial solve

    Passing null as the argument list turns the call into a checker. No field changes, and the return value reports whether the current values satisfy all active constraints.

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // not-run: fragment (bus_txn is the class above); Verilator 5.048 also rejects randomize(null)
    module top;
      bus_txn t = new();
      initial begin
        // Hand-build a transaction, e.g. replaying a captured trace
        t.addr = 32'h0000_1404;
        t.len  = 4;
    
        if (t.randomize(null))
          $display("trace txn is legal per class constraints");
        else
          $error("trace txn VIOLATES constraints");
    
        // Also useful after mutating one field of a random txn:
        t.len = 200;                       // outside [1:16]
        assert (!t.randomize(null));        // correctly reports illegal
      end
    endmodule

    The solver pins every rand field to its current value and asks whether that exact point lies inside the solution space. Nothing is written either way, which also makes it a debug tool for isolating contradictory combinations.

    Calling t.randomize(addr) restricts the solve set to the listed variables. Only addr is treated as random; every other field, including other rand fields, is frozen at its current value as state.

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // not-run: fragment (bus_txn is the class above); Verilator 5.048 also re-randomizes len in t.randomize(addr), so the last assert fails
    module top;
      bus_txn t = new();
      bit [7:0] keep_len;
      initial begin
        assert (t.randomize());            // full solve: addr and len
        keep_len = t.len;
    
        assert (t.randomize(addr));        // re-roll ONLY addr
        // len is frozen at keep_len; the solver picks a new addr that still
        // satisfies c_addr and c_len with that fixed len
    
        assert (t.len == keep_len);        // holds: len was not in the solve set
      end
    endmodule

    Not every simulator follows this yet: one of the simulators used here also re-randomized len in t.randomize(addr). Where a field must stay put in this course's runnable listings, it is frozen with rand_mode(0) instead, which the dynamic-control topic teaches.

    Two semantics matter here. A listed variable is randomized even if it is not declared rand, and this is the clean fix for randc slot burning.

    Constraints are never relaxed; only the set of free variables changes.

    Which tool for which draw

    Class randomization suits transactions: reusable objects with intrinsic legality rules. Scope randomization suits local, one-off decisions where a class would be ceremony. Plain draws suit single bounded values.

    Pick the lightest tool that can express the rule.
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // not-run: fragment (bus_txn is the class above; clk and drive() are not defined here)
    // Typical test-level mix of all three tools:
    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: plain draw
          @(posedge clk);
      end
    endtask

    Each call sits at the right weight class. The std randomize jointly picks count and mode under the implication, while the class call runs the transaction rules.

    $urandom_range draws a plain gap with no solver involved.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // not-run: fragment (clk and send_burst() are not defined here)
    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

    The with-clause is solved jointly: the solver picks a legal pair, not gap then burst. Variables from the surrounding scope that appear in the clause but not in the argument list are state variables, read and never written.

    $urandom returns a 32-bit unsigned value and $urandom_range bounds it inclusively, tolerating swapped arguments. They are thread-stable system functions, not solver calls: no constraints, no failure mode, far cheaper than a solve.

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // not-run: illustration with no self-check; Icarus also rejects a $urandom seed that is not a variable
    module top;
      bit [31:0] raw;
      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: arguments may be swapped
        raw  = $urandom(42);              // seed is an input expression: reseeds this
                                          // thread's RNG (unlike $random's inout seed)
    
        // 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
    endmodule

    $urandom_range

    std::randomize

    class randomize

    Constraints

    none

    with-clause

    class plus inline with

    Cross-variable

    no

    yes

    yes

    Can fail

    no

    yes, returns 0

    yes, returns 0

    Cost

    trivial

    solver call

    solver call

    Random stream

    calling thread

    calling thread

    the object

    Rule reuse

    none

    none, inline

    high, inside the class

    Typical use

    gaps, delays

    local joint picks

    transactions

    Testbench

    Predict first The bench below finishes with t.randomize() with { addr == 0; }, which contradicts c_addr. What does the call return, and what happens to addr and len?

    It returns 0, and both fields keep the values from the previous successful call. A failed solve commits nothing.

    A hundred checked solves, one contradiction, a joint std::randomize with an implication, and $urandom_range bounds.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    class bus_txn;
      rand bit [31:0] addr;
      rand bit [7:0]  len;
      constraint c_addr { addr inside {[32'h0000_1000 : 32'h0000_1FFF]}; }
      constraint c_len  { len inside {[1:16]}; addr[1:0] == 0; }
    endclass
    
    module tb_contract;
      bus_txn t = new();
      bit [31:0] keep_addr;
      bit [7:0]  keep_len;
      int ok, n_txns, mode, gap;
      initial begin
        // 1. The idiom: check every call. A success commits a legal pair.
        repeat (100) begin
          if (!t.randomize()) $fatal(1, "FAIL: randomize failed");
          if (t.addr < 32'h1000 || t.addr > 32'h1FFF || t.addr[1:0] != 0 || t.len < 1 || t.len > 16)
            $fatal(1, "FAIL: illegal txn addr=%h len=%0d", t.addr, t.len);
        end
        // 2. A contradiction returns 0 and commits nothing.
        keep_addr = t.addr;
        keep_len  = t.len;
        ok = t.randomize() with { addr == 0; };
        $display("contradiction: randomize returned %0d, addr %h -> %h, len %0d -> %0d", ok, keep_addr, t.addr, keep_len, t.len);
        if (ok != 0 || t.addr != keep_addr || t.len != keep_len) $fatal(1, "FAIL: a failed solve changed the object");
        // 3. std::randomize solves local variables jointly.
        repeat (100) begin
          if (!std::randomize(n_txns, mode) with { n_txns inside {[10:50]}; mode inside {[0:2]}; (mode == 2) -> n_txns > 30; })
            $fatal(1, "FAIL: std::randomize failed");
          if (mode == 2 && n_txns <= 30) $fatal(1, "FAIL: stress mode with only %0d txns", n_txns);
        end
        // 4. $urandom_range: bounds inclusive, arguments in either order.
        repeat (100) begin
          gap = $urandom_range(5, 0);
          if (gap < 0 || gap > 5) $fatal(1, "FAIL: gap %0d", gap);
        end
        $display("PASS: 100 legal solves, a failed solve left the object untouched, std::randomize and $urandom_range held their rules");
        $finish;
      end
    endmodule
    Terminal
    %Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t3.sv:4: Unsatisfied constraint: 'constraint c_addr { addr inside {[32'h0000_1000 : 32'h0000_1FFF]}; }'
    %Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t3.sv:23: Unsatisfied constraint: 'ok = t.randomize() with { addr == 0; };'
    contradiction: randomize returned 0, addr 00001f40 -> 00001f40, len 15 -> 15
    PASS: 100 legal solves, a failed solve left the object untouched, std::randomize and $urandom_range held their rules

    Common pitfall

    Discarding the return value re-sends stale transactions with no error anywhere. And where assertion elision can remove calls, prefer the safe form: if (!t.randomize()) $error(...).

    Interview

    Expect contract questions that separate the call shapes.

    • randomize returns 1 or 0, and on failure all fields keep prior values: the solve is atomic.

    • A hand-built transaction is validated without changes by randomize with a null argument.

    • One field is re-rolled while the rest freeze by naming it in the argument list.

    • A listed variable is randomized even when it is not declared rand.

    • randomize is a built-in method that cannot be overridden; pre and post hooks carry the customization.

    Check yourself randomize with a field list names a non-rand field. Is that field solved?

    Yes. The argument list overrides the declaration for that call: listed variables are randomized, and every other rand field freezes as state.

    Check yourself Why is assert(t.randomize()) unsafe in flows with aggressive assertion elision?

    Elision can remove the assert together with its call, so randomize never runs. The if (!t.randomize()) $error form keeps the call on every flow.

    Check yourself Two locals must satisfy a plus b equals 100. Why can two $urandom_range calls not do it?

    Independent draws cannot express a relationship between variables. One std::randomize with a plus-equals constraint solves the pair jointly.

    Project step

    Make pkt_txn random: rand kind, dest, len and a fixed eight-byte payload, with len limited to 1 to 8. Check every randomize() call, and over 500 calls confirm that every length and every kind appears.

    One way to write it:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
    
    // PacketFIFO transaction with rand fields: the solver now builds legal packets.
    class pkt_txn;
      rand kind_e     kind;
      rand bit [1:0]  dest;
      rand bit [3:0]  len;
      rand bit [7:0]  payload[8];          // fixed size; only the first len bytes are sent
      constraint c_len  { len inside {[1:8]}; }
    
      function bit [7:0] header();
        return {kind, dest, len};
      endfunction
    
      function bit [7:0] check_byte();
        bit [7:0] c = header();
        for (int i = 0; i < len; i++) c ^= payload[i];
        return c;
      endfunction
    endclass
    
    module tb_pkt_rand;
      pkt_txn p = new();
      int lens[int];
      int kinds[int];
      initial begin
        repeat (500) begin
          if (!p.randomize()) $fatal(1, "FAIL: randomize failed");   // every call checked
          if (p.len < 1 || p.len > 8) $fatal(1, "FAIL: illegal len %0d", p.len);
          lens[int'(p.len)]++;
          kinds[int'(p.kind)]++;
        end
        $display("lengths seen: %0d of 8, kinds seen: %0d of 3", lens.num(), kinds.num());
        if (lens.num() != 8 || kinds.num() != 3) $fatal(1, "FAIL: some legal value never appeared");
        $display("PASS: 500 checked solves gave legal packets covering every length and kind");
        $finish;
      end
    endmodule
    Terminal
    lengths seen: 8 of 8, kinds seen: 3 of 3
    PASS: 500 checked solves gave legal packets covering every length and kind

    Remember

    • randomize() returns 1 with every field updated, or 0 with nothing changed.

    • Plain (non-rand) fields are read by the solver and never written.

    • std::randomize with solves local variables jointly; $urandom_range never fails.

    Rule of thumb: no randomize() without a check.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator prints a harmless WIDTHTRUNC warning for if (!obj.randomize()), because randomize() returns an int, and an UNSATCONSTR warning when a solve fails on purpose. Icarus 13 does not support constraints.

    Not run: randomize(null) is rejected by Verilator 5.048 ("Unsupported"), and its randomize(addr) re-randomized len as well, so the field-list listing is shown but not run.

    Related topics

    • Next: Hooks and Nested Objects

    Previous

    66 / 259

    Next: Hooks and Nested Objects

    Up next

    Up nextHooks and Nested Objects

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Random Stability and Seeding