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.
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
endmodulePASS: knobs reshaped the second solve, len=2The 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.
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.
// 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; }
endclassTwo 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.
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
endmodulemax_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 transactionThe 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.
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.
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.
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
endmoduletotal_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 fieldHooks 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 ( | Rand handle ( | |
|---|---|---|
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 |
|
No. State is read-only input to the solve: the rand variables must fit around it, or randomize() returns 0.
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.
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.