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
      • rand_mode(): Freezing Fields
      • constraint_mode(): Toggling Blocks
      • Inline Constraints: randomize() with
      • Constraint Override via Inheritance
      • State Variables and Cross-Object
    • 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

‹Dynamic Constraint Control
  1. Topics
  2. SystemVerilog
  3. Constraint Randomization — Interview-Critical Deep Dive
  4. Dynamic Constraint Control
  5. State Variables and Cross-Object

Part 3 · Constraint Randomization · Intermediate

State Variables and Cross-Object

Non-rand knobs in constraints, cross-object guards, nested rand solves, and solve order.

On this page

    Why this matters

    Halfway through a test the length limit changed, and the next packets still used the old one. The knob had been set after the call, and knobs only apply to the calls that come after them.

    After this lesson you can: drive constraints from non-rand knobs and history, and tell a value read as state from one solved together with a rand child.

    Knobs are solve-time constants

    Any non-rand property in a constraint is read as a constant at solve time. The solver never assigns it, so tests set knobs procedurally and every later randomize() solves against the new 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
    class dma_txn;
      int unsigned max_len = 64;   // state knobs, set by config
      bit allow_wr = 1;
      rand bit [9:0] len;
      rand bit write;
      constraint len_c   { len inside {[1:max_len]}; }   // knob gates the range
      constraint write_c { !allow_wr -> write == 0; }    // knob gates a mode
    endclass
    module tb_knob_demo;
      initial begin
        automatic dma_txn t = new();
        void'(t.randomize());   // len in [1:64], writes allowed
        if (!(t.len inside {[1:64]})) $fatal(1, "FAIL: len=%0d", t.len);
        t.max_len = 8;          // reconfigure between calls
        t.allow_wr = 0;
        void'(t.randomize());   // len in [1:8], write forced to 0
        if (!(t.len inside {[1:8]}) || t.write != 0)
          $fatal(1, "FAIL: len=%0d write=%0d after the knob change", t.len, t.write);
        $display("PASS: knobs reshaped the second solve, len=%0d", t.len);
        $finish;
      end
    endmodule
    Terminal
    PASS: knobs reshaped the second solve, len=2

    The knob value at the moment of the call is what counts. A knob can also empty the space, as max_len = 0 would, which fails randomize() like any contradiction.

    Handles to other objects

    Constraints can reach through handles, such as cfg.max_burst or prev.addr. A handle that is not being randomized in the call is more state: its fields are read as constants.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: the standard form, reading through handles
    class bus_txn;
      bus_cfg cfg;                // shared config, read as state
      bus_txn prev;               // previous txn, read as history
      rand bit [9:0]  burst;
      rand bit [31:0] addr;
      constraint burst_c { burst <= cfg.max_burst; }
      constraint order_c { prev != null -> addr > prev.addr; }
    endclass

    Two simulator problems make this form fragile in practice. One of the simulators used here dereferenced prev even under the prev != null guard and stopped the run, and once a class read cfg.max_burst through a handle, cfg.randomize() stopped changing it. The bench below copies both values into plain fields of the transaction, which behaves the same everywhere.

    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
    class bus_cfg;
      rand bit [9:0] max_burst;   // rand here, but state from the txn's point of view
      constraint c { max_burst inside {[8:64]}; }
    endclass
    
    class bus_txn;
      bit [9:0]  max_burst;       // config and history copied in as plain state
      bit        have_prev;
      bit [31:0] prev_addr;
      rand bit [9:0]  burst;
      rand bit [31:0] addr;
      constraint burst_c { burst <= max_burst; }
      constraint order_c { have_prev -> (addr > prev_addr && addr - prev_addr <= 4096); }
      constraint room_c  { addr < 32'h1000_0000; }
    endclass
    
    module tb_crossobj;
      bus_cfg cfg = new();
      bus_txn t, last;
      initial begin
        if (!cfg.randomize()) $fatal(1, "FAIL: cfg randomize failed");
        last = null;
        for (int i = 0; i < 20; i++) begin
          t = new();
          t.max_burst = cfg.max_burst;
          t.have_prev = (last != null);
          if (last != null) t.prev_addr = last.addr;
          if (!t.randomize()) $fatal(1, "FAIL: txn %0d randomize failed", i);
          if (t.burst > cfg.max_burst) $fatal(1, "FAIL: burst %0d above max %0d", t.burst, cfg.max_burst);
          if (last != null && t.addr <= last.addr) $fatal(1, "FAIL: addr did not increase");
          last = t;
        end
        $display("max_burst = %0d; 20 transactions, each address above the one before", cfg.max_burst);
        $display("PASS: the copied max_burst and previous address were read as state, and have_prev covered the first transaction");
        $finish;
      end
    endmodule
    Terminal
    max_burst = 40; 20 transactions, each address above the one before
    PASS: the copied max_burst and previous address were read as state, and have_prev covered the first transaction

    The prev != null guard is idiomatic and required: dereferencing a null handle at solve time is a runtime error. The first transaction leaves the implication satisfied without a previous address.

    One simultaneous solve

    A member handle declared rand joins the parent solve: parent and child variables form one constraint system solved together and bidirectionally. A parent rule on a child field can therefore push back on parent fields.

    Predict first frame.total_len == hdr_len + pl.nbytes, with hdr_len in 4 to 12 and pl.nbytes in 1 to 64. A call asks for total_len == 76. What must hdr_len and pl.nbytes be?

    12 and 64: 76 is the largest total the two ranges allow. The parent's request pushes the child's field, because a rand child is solved together with the parent.

    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
    class payload;
      rand bit [7:0] nbytes;
      constraint c { nbytes inside {[1:64]}; }
    endclass
    
    class frame;
      rand payload pl;         // rand handle: solved WITH the frame
      rand bit [9:0] total_len;
      rand bit [3:0] hdr_len;
      constraint glue_c {
        total_len == hdr_len + pl.nbytes;   // spans parent AND child
        hdr_len inside {[4:12]};
      }
      function new(); pl = new(); endfunction   // allocate before solving
    endclass
    
    module tb_nested_frame;
      frame f = new();
      int ok;
      initial begin
        repeat (200) begin
          if (!f.randomize()) $fatal(1, "FAIL: randomize failed");
          if (f.total_len != f.hdr_len + f.pl.nbytes || f.pl.nbytes < 1 || f.pl.nbytes > 64)
            $fatal(1, "FAIL: total %0d hdr %0d nbytes %0d", f.total_len, f.hdr_len, f.pl.nbytes);
        end
        ok = f.randomize() with { total_len == 76; };      // the parent pushes the child: nbytes must be 64
        $display("total_len == 76 gave hdr_len %0d and nbytes %0d", f.hdr_len, f.pl.nbytes);
        if (!ok || f.pl.nbytes != 64 || f.hdr_len != 12) $fatal(1, "FAIL: the parent did not push the child");
        $display("PASS: one solve kept total_len == hdr_len + pl.nbytes, and fixing total_len pushed the child's field");
        $finish;
      end
    endmodule
    Terminal
    total_len == 76 gave hdr_len 12 and nbytes 64
    PASS: one solve kept total_len == hdr_len + pl.nbytes, and fixing total_len pushed the child's field

    Hooks and Nested Objects covers building nested objects and the null-handle trap; this lesson adds only that one solve covers both objects.

    Non-rand handle or field (cfg.max_burst)

    Rand handle (frame.pl)

    In the solve?

    No: read as a constant

    Yes: joins one solve with the parent

    Direction

    One way: the transaction adapts to it

    Both ways: the child can push the parent and back

    Turned off by

    Nothing: it was never solved

    pl.rand_mode(0) makes the whole child state until rand_mode(1)

    Common mistakes

    Common pitfall

    A rand child that was never constructed is a null dereference at solve time, and knobs set after randomize() apply to the next call, not the last one. State never moves to avoid a contradiction, so a knob that empties the space fails the solve.

    Interview

    Likely questions: how a non-rand variable behaves in a constraint (solve-time constant), what changes when a member handle is rand (one bidirectional solve), and how null handles are guarded (handle != null implications).

    Check yourself Can the solver adjust a non-rand knob to avoid a contradiction?

    No. State is read-only input to the solve: the rand variables must fit around it, or randomize() returns 0.

    Check yourself A parent constraint mentions a rand child's field. Can the child value push the parent value?

    Yes. Rand children join one simultaneous bidirectional system with the parent. A non-rand child would instead freeze its fields as state.

    In the layer map, state answers what configuration the solve reads as constants. Use knobs for config-dependent spaces, never for per-call intent.

    Remember

    • A non-rand field in a constraint is a constant at the moment of the call.

    • A rand child object is solved together with its parent, in both directions.

    • Knobs set after randomize() affect the next call, not the last one.

    Rule of thumb: configure, then randomize; never the other way round.

    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 dereferenced prev despite the prev != null guard (both the -> and the if form) and stopped the run; and once a class read cfg.max_burst through a handle, cfg.randomize() returned 1 without changing it. The bench copies both values into plain fields.

    The nested frame listing, earlier marked as a solver error, ran correctly on 2026-10-18, including a parent value pushing the child's field.

    Related topics

    • Next topic: Randomizing Arrays and Queues

    Previous

    77 / 259

    Next: Randomizing Arrays and Queues

    Up next

    Up nextRandomizing Arrays and Queues

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • rand_mode(): Freezing Fields
    • constraint_mode(): Toggling Blocks
    • Inline Constraints: randomize() with