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. Hooks and Nested Objects

Part 3 · Constraint Randomization · Intermediate

Hooks and Nested Objects

Pre and post hooks, one global solve, null handles and object array patterns.

On this page

    Why this matters

    Every derived packet went out with a stale checksum. Its post_randomize override forgot to call super, so the base class never recomputed the value.

    After this lesson you can: put setup in pre_randomize and derivations in post_randomize, call super in overrides, and build nested rand objects so the solver can reach them.

    Setup before, derive after

    Every class may define pre_randomize and post_randomize hooks. randomize invokes them automatically: pre runs before the solver, post runs after it succeeds.

    Their division of labor follows from the solver contract. Pre sets up state the solver reads, such as mode flags and rand mode switches. Post derives values the solver cannot produce, such as checksums over the solved payload.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // not-run: class only, no module, and crc32() is not defined here
    class eth_frame;
      rand bit [7:0]  payload[];
      rand bit [11:0] vlan_id;
      bit  [31:0]     fcs;          // NOT rand: derived, not solved
      bit             vlan_en;      // state knob set by the test
    
      constraint c_size { payload.size() inside {[46:1500]}; }
    
      function void pre_randomize();
        // setup the solver's world BEFORE it runs
        vlan_id.rand_mode(vlan_en);   // freeze vlan_id unless enabled; the mode
                                      // is sticky, so set it on every call
      endfunction
    
      function void post_randomize();
        // derive what the solver cannot: a function of the random result
        fcs = crc32(payload);
      endfunction
    endclass

    Pre runs unconditionally, before success is known. Post runs only on success: deriving from stale data after a failed solve would corrupt the frame.

    Hook

    Runs

    Job

    pre_randomize

    Always, even on failure

    Set up solver-readable state: knobs, rand mode and constraint mode switches

    post_randomize

    Only on success

    Derive non-rand fields from solved values: CRC, parity, timestamps, logs

    One solve, many hooks

    Hooks dispatch to the most-derived override in all modern tools, so each override should call its super hook. When the solver recurses into rand handles, there is still one global solve for the whole object tree.

    Each participating object's post hook runs once after that solve succeeds. The standard does not fix their relative order, so portable code never relies on it.

    Pre hooks, one solve for the whole object tree, then each post hook once.

    The bench at the end of this lesson runs exactly this: a derived post_randomize called through a base handle.

    Dispatching through a base-typed handle to the derived override proves the virtual behavior. Omitting the super call is the classic bug.

    Without it, base derivations silently stop happening in every derived transaction.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet: the trap, shown, never run
    class msg;
      rand bit [7:0] data[4];
      bit            parity;
    
      function void post_randomize();
        parity = ^{data[0], data[1], data[2], data[3]};   // derive: fine
    
        // THE TRAP: re-randomizing inside the hook re-enters the hooks,
        // re-solves everything, and burns randc slots. Never do this:
        // if (parity) assert (this.randomize());
      endfunction
    endclass

    An inner randomize triggers the full sequence again, so post re-enters itself. Without a guard this recurses, and with one it still replaces the outer results.

    Retry at the call site instead, or express the rule as a constraint.

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // snippet: msg is the class above; external_filter_ok() stands for any outside check
    // Call-site retry pattern (instead of hook recursion):
    module top;
      msg m = new();
      int tries = 0;
      bit ok;
      initial begin
        do begin
          assert (m.randomize());
          tries++;
          ok = external_filter_ok(m);
        end while (!ok && tries < 100);
        // Test ok, not tries == 100: the 100th try may have succeeded.
        if (!ok) $error("could not produce acceptable msg");
      end
    endmodule
    // Better still: encode the filter rule as a constraint,
    // so the solver only ever produces acceptable messages.

    Null handles and object arrays

    A rand class handle is never re-pointed by the solver. randomize recurses into the referenced object and folds its fields and constraints into the one flattened solve.

    Outer constraints may reference inner fields directly. The header and packet below are co-determined in a single solve, not randomized in two passes.

    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
    27
    28
    29
    30
    31
    class header;
      rand bit [3:0] version;
      rand bit [7:0] len;
      constraint c_ver { version inside {4, 6}; }
    endclass
    
    class packet;
      rand header   h;             // rand handle: the solver recurses
      rand bit [7:0] data[];
    
      // cross-object constraint: outer references inner field
      constraint c_size { data.size() == h.len; }
    
      function new();
        h = new();                 // construct in new: never null
      endfunction
    endclass
    
    module top;
      packet p = new();
      initial begin
        assert (p.randomize());
        // ONE solve chose version, len, data size and contents,
        // with data.size() == h.len holding by construction
        if (p.data.size() != p.h.len || !(p.h.version inside {4, 6}))
          $fatal(1, "FAIL: size=%0d len=%0d version=%0d",
                 p.data.size(), p.h.len, p.h.version);
        $display("PASS: data.size() == h.len == %0d after one solve", p.h.len);
        $finish;
      end
    endmodule
    Terminal
    PASS: data.size() == h.len == 29 after one solve

    A null rand handle is silently skipped, and randomize still returns 1. Nothing forces an error unless a constraint dereferences the handle, so the bug surfaces far from its cause.

    A forgotten new() is silent at randomize time and loud much later.
    1. Prevent it: construct every nested rand object in new().

    2. Catch it early: check the handle in pre_randomize(), as the bench below does.

    3. Model optional parts with an enable bit and if constraints, never with a handle that is sometimes null.

    Verilator 5.048 follows this only when the handle's class has no constraints of its own. When it has some, the run stops with "Null pointer dereferenced" at the randomize() call: louder, and closer to the cause, but not what the standard says. Build every rand handle in new() and both tools agree.

    Skipping instead of erroring exists for genuinely optional sub-objects. The cost is that a forgotten construction looks identical, so null should mean bug, never mode.

    Arrays of objects follow the same rule: the solver randomizes fields of every non-null element but never constructs elements. Size the array procedurally, construct each element, then randomize.

    A rand dynamic array can still have its size solved under constraints. Only object creation is off limits to the solver.

    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
    27
    28
    29
    // not-run: Verilator 5.048 fails to build a constraint over an array of class handles (C++ compile error)
    class beat;
      rand bit [31:0] data;
      rand bit        last;
    endclass
    
    class burst;
      rand beat beats[];           // array of handles
      rand bit [3:0] blen;
    
      constraint c_last {          // only the final beat has last == 1
        foreach (beats[i])
          beats[i].last == (i == beats.size() - 1);
      }
    
      // Size and construct in a controlled step BEFORE the solve:
      function void build(int n);
        beats = new[n];
        foreach (beats[i]) beats[i] = new();
      endfunction
    endclass
    
    module top;
      burst b = new();
      initial begin
        b.build(8);                  // 1. size plus construct every element
        assert (b.randomize());      // 2. one solve: all beats data and last bits
      end
    endmodule

    With eight constructed elements, the foreach expands to eight equality constraints in the flattened problem. A null element would be skipped while the constraint dereferencing it blows up instead.

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: b is the burst object above
    // When the element COUNT itself should be random, use two passes:
    module top;
      int n;
      initial begin
        assert (std::randomize(n) with { n inside {[1:16]}; });   // pass 1: size
        b.build(n);                                               // construct
        assert (b.randomize());                                   // pass 2: fields
      end
    endmodule
    // Trade-off: constraints cannot couple n with beat contents in one
    // solve. The solver never creates objects, so this split is the price.

    Testbench

    Two benches. The first pins the null defense into pre_randomize. The second runs the hook discipline: a derived post_randomize reached through a base handle, a derivation the solver cannot make, and a failed solve.

    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
    class config_c;
      rand bit [2:0] qos;
    endclass
    
    class txn_checked;
      rand config_c cfg;
      rand bit [7:0] addr;
      function new();
        cfg = new();
      endfunction
      function void pre_randomize();
        if (cfg == null) $fatal(1, "FAIL: cfg is null before the solve");
      endfunction
    endclass
    
    module tb_pre_null;
      txn_checked t = new();
      initial begin
        if (!t.randomize()) $fatal(1, "FAIL: randomize failed");
        if (t.cfg == null) $fatal(1, "FAIL: cfg null after a successful solve");
        $display("PASS: cfg was constructed in new() and survived the solve");
        $finish;
      end
    endmodule
    Terminal
    PASS: cfg was constructed in new() and survived the solve
    Predict first The second bench calls h.randomize() three times through a base handle, then once with a constraint that contradicts c_kind. How many entries does kind_log hold, and does kind change on the last call?

    Three, one per successful solve, because the derived post hook calls super and then logs. The last call fails, so kind keeps its value.

    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
    41
    42
    43
    44
    class base_pkt;
      rand bit [7:0] kind;
      bit  [7:0]     data_par;
      rand bit [7:0] data[4];
      int pre_n, post_n;
      constraint c_kind { kind < 16; }
      function void pre_randomize();
        pre_n++;
      endfunction
      function void post_randomize();
        post_n++;
        data_par = data[0] ^ data[1] ^ data[2] ^ data[3];   // derive: the solver cannot
      endfunction
    endclass
    
    class derived_pkt extends base_pkt;
      bit [7:0] kind_log[$];
      function void post_randomize();
        super.post_randomize();        // keep the base derivation
        kind_log.push_back(kind);
      endfunction
    endclass
    
    module tb_hooks;
      base_pkt    h;
      derived_pkt d;
      bit [7:0]   keep;
      int ok;
      initial begin
        d = new();
        h = d;                         // base handle, derived object
        repeat (3) if (!h.randomize()) $fatal(1, "FAIL: randomize failed");
        $display("after 3 solves: pre=%0d post=%0d logged=%0d parity ok=%0d", d.pre_n, d.post_n, d.kind_log.size(),
                 d.data_par == (d.data[0] ^ d.data[1] ^ d.data[2] ^ d.data[3]));
        if (d.pre_n != 3 || d.post_n != 3 || d.kind_log.size() != 3) $fatal(1, "FAIL: hooks did not run once per solve");
        if (d.data_par != (d.data[0] ^ d.data[1] ^ d.data[2] ^ d.data[3])) $fatal(1, "FAIL: post did not derive the parity");
        keep = d.kind;
        ok = h.randomize() with { kind > 100; };       // contradicts c_kind
        $display("contradiction: returned %0d, kind %0d -> %0d, pre=%0d post=%0d", ok, keep, d.kind, d.pre_n, d.post_n);
        if (ok != 0 || d.kind != keep) $fatal(1, "FAIL: a failed solve changed kind");
        $display("PASS: derived post ran through a base handle and kept the base parity; the failed solve changed nothing");
        $finish;
      end
    endmodule
    Terminal
    after 3 solves: pre=3 post=3 logged=3 parity ok=1
    %Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t7.sv:6: Unsatisfied constraint: 'constraint c_kind { kind < 16; }'
    %Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t7.sv:38: Unsatisfied constraint: 'ok = h.randomize() with { kind > 100; };        '
    contradiction: returned 0, kind 0 -> 0, pre=4 post=4
    PASS: derived post ran through a base handle and kept the base parity; the failed solve changed nothing

    The standard says post_randomize does not run when the solve fails. One of the simulators used here runs it anyway, which is why the bench checks the fields after the failed call rather than the hook count.

    Common pitfall

    Overriding a hook without calling super silently drops the base derivation in every derived object. Post is also skipped on failure, so derived fields go stale together with the random fields.

    Interview

    Hooks connect solver mechanics, inheritance and discipline in one topic.

    • post_randomize does not run when the solve fails; pre_randomize always runs.

    • A checksum over random payload belongs in post_randomize: it is a function of solved values.

    • Fix-ups in post bypass the solver, so other constraints cannot react and check-only calls disagree.

    • Hooks dispatch to the most-derived override in modern tools; overrides call super.

    • randomize inside a hook re-enters the hooks: retry at the call site instead.

    Check yourself A derived packet randomize fails. Which hooks ran, and what is the state of the derived CRC?

    Pre ran and post did not. Random fields are unchanged and the CRC is stale, since post only derives on success.

    Check yourself An outer constraint references an inner object's field. How many solver calls produce the pair?

    One. Nested rand objects join a single flattened solve, so the cross-object rule holds by construction.

    Check yourself When is a null rand handle the right model for an optional sub-object?

    Never. A null handle is silently skipped, which is indistinguishable from a forgotten construction. Use a non-rand enable bit with if-constraints.

    Remember

    • pre_randomize sets up what the solver reads; post_randomize derives from the result.

    • Overrides call super first, or the base derivation silently stops.

    • A rand handle left null is skipped and the call still returns 1.

    Rule of thumb: construct every nested rand object in new().

    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 runs post_randomize after a failed solve too (the standard says it must not), so the bench checks the fields after the failure, not the hook count.

    Not run: a constraint over an array of class handles; Verilator 5.048 ignored each element's own constraints in our probe.

    Related topics

    • Next: Random Stability and Seeding

    Previous

    67 / 259

    Next: Random Stability and Seeding

    Up next

    Up nextRandom Stability and Seeding

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • rand vs randc