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()
    • Constraint Operators & Expressions
    • Dynamic Constraint Control
    • Randomizing Arrays and Queues
      • Constraining Array Size
      • foreach Constraints
      • sum(), Aggregate Constraints
      • unique Constraints
      • Arrays of rand Objects
    • 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

‹Randomizing Arrays and Queues
  1. Topics
  2. SystemVerilog
  3. Constraint Randomization — Interview-Critical Deep Dive
  4. Randomizing Arrays and Queues
  5. Arrays of rand Objects

Part 3 · Constraint Randomization · Intermediate

Arrays of rand Objects

Build object arrays before randomize, then relate their fields in one call or a loop.

On this page

    Why this matters

    A burst sequence randomized an array of 8 handles and crashed on the first packet. The array had been sized with new[8], which made 8 null handles and no objects.

    After this lesson you can: build object arrays before randomizing, choose one joint call or a per-object loop, and shape traffic with a gap array.

    The solver never calls new

    Declaring rand txn seq[] lets randomize() solve the rand fields of every non-null object in the array. A null handle stays null, and nothing of it enters the solve.

    So the pattern is build first, randomize second: size the array, new() every element, then call randomize(). The object count is procedural, never a solver choice.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet
    class burst;
      rand txn seq[];
      function void pre_randomize();
        seq = new[4];                     // 4 null handles
        foreach (seq[i]) seq[i] = new();  // now 4 objects
      endfunction
      constraint c_rel {
        seq[0].is_write == 1;                              // first is a write
        foreach (seq[i]) if (i > 0)
          longint'(seq[i].addr) >= longint'(seq[i-1].addr) + seq[i-1].len * 4;
      }
    endclass

    This is the standard form, and it is how a commercial simulator runs a burst. Verilator 5.048 ignored each element's own constraints in an array of rand handles, so the fragment above is not run here. The loop form below runs everywhere.

    Check yourself randomize() runs on an array of null handles. What is solved?

    Nothing in them. Null handles have no fields in the solve, so every element needs its own new() first.

    One call or a loop

    A loop of seq[i].randomize() calls solves each object alone, with earlier objects already fixed. Backward rules, such as a destination different from the previous one, go in each call's with clause using local:: to name the previous object.

    Two ways to randomize a burst.

    Rule

    One call

    Loop

    dest differs from the previous packet

    Yes

    Yes, via with and local::

    The first packet is DATA

    Yes

    Yes, in the first call

    Lengths total exactly 32

    Yes

    No: the last packet cannot fix an earlier one

    Exactly two ERR packets

    Yes

    Only with a count kept by hand

    Check yourself Why can a per-object loop not enforce “lengths total 32”?

    Each call solves one object with the past fixed. Only one joint solve sees every length at once, and the loop has no such solve.

    Shape traffic with a gap array

    A rand gap array beside the packets sets idle cycles before each one. All-zero gaps stress back-to-back traffic, bounded gaps throttle, and a mostly-zero dist gives bursty patterns.

    A guaranteed back-to-back pair needs care. A counting sum() over the same elements as the dist made about a third of Verilator's calls fail, though legal answers existed. A rand index b2b that forces one gap to zero, with the dist guarded off that slot, works.

    Project step

    Build a PacketFIFO burst: 8 packets, the first one DATA, no two neighbours with the same dest, and a gap array where gap 0 is zero, the rest follow 70% zero, 25% one to three cycles, 5% ten to twenty cycles, and at least one later gap is always zero. Check 200 bursts and print the measured gap shares.

    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;
    
    class pkt_txn;
      rand kind_e    kind;
      rand bit [1:0] dest;
      rand bit [3:0] len;
      constraint legal_c { len inside {[1:8]}; dest inside {[0:2]}; }
    endclass
    
    class pkt_burst;
      pkt_txn        seq[];                            // built here, solved one by one
      rand bit [7:0] gap[8];                           // idle cycles before seq[i]
      rand bit [2:0] b2b;                              // where the guaranteed pair sits
      int            n;
      constraint c_shape {
        gap[0] == 0;                                   // start at once
        foreach (gap[i])
          if (i > 0 && i != b2b) gap[i] dist { 0 :/ 70, [1:3] :/ 25, [10:20] :/ 5 };
        b2b inside {[1:7]};                            // one slot forced back-to-back
        foreach (gap[i]) if (i == b2b) gap[i] == 0;
      }
      function void build(int count);
        n = count;
        seq = new[n];
        foreach (seq[i]) seq[i] = new();               // randomize() never calls new()
      endfunction
      function bit fill();
        if (randomize() == 0) return 0;                // the gap array, in one solve
        foreach (seq[i]) begin
          if (i == 0) begin
            if (!seq[i].randomize() with { kind == DATA; }) return 0;
          end else begin
            if (!seq[i].randomize() with { dest != local::seq[i-1].dest; }) return 0;
          end
        end
        return 1;
      endfunction
    endclass
    
    Terminal
    gaps after the first: 70% zero, 60 long ones in 1400
    last burst gaps: 0 0 0 0 0 0 0 0
    PASS: 200 bursts open with DATA, never repeat a dest, and always hold a back-to-back pair

    Check the measured shares against the arithmetic. Six of the seven later gaps follow the dist, so long gaps should be 6 × 5% out of 7, about 4.3%, and Verilator gave 4.3%. Zeros should be 6 × 70% plus the forced one, out of 7, about 74%; Verilator gave about 70%, a small drift like the one in dist: Weighted Distributions. The driver then waits gap[i] cycles before sending seq[i].

    Common pitfall

    seq = new[N] makes N null handles, not N objects. Skipping the per-element new() loop leaves nulls that fail at the first use.

    Interview

    Expect “why can the solver not choose the object count” and “one call versus a loop”. The answers are that randomize() never calls new(), and that only one joint solve can enforce totals across objects.

    Check yourself How is the object count varied across tests if the solver cannot choose it?

    Procedurally: pick the count in the test or in pre_randomize, or build the most you need and gate them with a rand valid count.

    Remember

    • randomize() never calls new(): build every object first.

    • One joint call can enforce totals; a loop only backward rules.

    • A gap array beside the packets shapes the timing.

    Rule of thumb: construct, then randomize, then check every object.

    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.

    Verilator 5.048 ignored element constraints in arrays of rand handles, so the PacketFIFO burst uses the loop form. A dist plus a counting sum on the same gaps failed about a third of calls; the bench forces one gap with a rand index and keeps the dist off it.

    Related topics

    • Next topic: How the Constraint Solver Works

    Previous

    82 / 259

    Next: How the Constraint Solver Works

    Up next

    Up nextHow the Constraint Solver Works

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Constraining Array Size
    • foreach Constraints
    • sum(), Aggregate Constraints