Part 3 · Constraint Randomization · Intermediate
State Variables & Cross-Object Constraints
Non-rand state in constraints, config knobs gating ranges, obj.field references, nested rand objects, and global solve order.
Non-rand state variables: parameterized constraints
Any non-rand class property referenced in a constraint is read as a constant at solve time — the solver never assigns it, it just uses its current value. This turns plain variables into configuration knobs: a test sets the knob procedurally, and every subsequent randomize() solves against the new value. It is the lightest-weight control mechanism of all — no blocks to toggle, no subclasses, no inline repetition — and the backbone of every “knobs object” style environment.
class dma_txn;
// ---- state knobs (NOT rand): set by test/config ----
int unsigned max_len = 64;
bit allow_wr = 1;
// ---- solved fields ----
rand bit [9:0] len;
rand bit write;
constraint len_c { len inside {[1:max_len]}; } // knob gates range
constraint write_c { !allow_wr -> write == 0; } // knob gates a mode
endclass
dma_txn t = new();
void'(t.randomize()); // len ∈ [1:64], writes allowed
t.max_len = 8; // reconfigure between calls
t.allow_wr = 0;
void'(t.randomize()); // len ∈ [1:8], write forced to 0Because the knob is read fresh at each solve, the value at the moment of the randomize() call is what counts — set knobs before randomizing, and remember a knob can make the space empty (max_len = 0 above) just like any contradiction.
Referencing other objects' fields
Constraint expressions can reach through object handles: cfg.max_burst or prev.addr. If the referenced object is NOT being randomized in this call, its fields are simply more state — read as constants. This gives you constraints against a shared config object, or against the previous transaction for ordered stimulus. The handle must be non-null at solve time; a null dereference in a constraint is a runtime error.
class bus_cfg;
rand bit [9:0] max_burst; // rand here, but state from txn's view
constraint c { max_burst inside {[8:64]}; }
endclass
class bus_txn;
bus_cfg cfg; // handle to shared config (state)
bus_txn prev; // handle to previous txn (state)
rand bit [9:0] burst;
rand bit [31:0] addr;
constraint burst_c { burst <= cfg.max_burst; } // cross-object
constraint order_c {
prev != null -> addr > prev.addr; // sequencing
}
endclass
// cfg was randomized ONCE at env setup; each txn.randomize() reads
// cfg.max_burst as a constant. prev.addr likewise — already-solved history.The prev != null guard is essential and idiomatic: handles are legal in constraint boolean expressions exactly so you can gate dereferences. The first transaction (prev == null) leaves order_c satisfied vacuously.
Nested rand objects: one big simultaneous solve
If an object handle is declared rand, calling randomize() on the parent recursively randomizes the child in the same solver invocation — parent and child variables become one constraint system, solved simultaneously and bidirectionally. Constraints in the parent that mention child fields (historically called global constraints) connect the two; a child-field constraint can push back on parent fields, exactly like any other bidirectional relation.
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 — global
hdr_len inside {[4:12]};
}
function new(); pl = new(); endfunction // must allocate before solve!
endclass
// frame.randomize() solves {pl.nbytes, total_len, hdr_len} TOGETHER:
// e.g. inline 'with { total_len == 20; }' can force pl.nbytes ∈ [8:16].
// Contrast: if pl were NOT rand, pl.nbytes would be frozen state and
// total_len would have to adapt around its current value.STATE vs NESTED-RAND — WHO IS IN THE SOLVE?
non-rand handle / field rand handle
─────────────────────── ────────────────────────────
cfg.max_burst, prev.addr frame.pl (pl.nbytes)
│ │
▼ ▼
READ as constants JOIN the constraint system
one-way influence ──► ◄── bidirectional ──►
(txn adapts to them) (child can push parent fields)
solve order inside one randomize() call:
1. randc variables (parent + nested, cycled independently)
2. ALL rand variables of parent + rand children: ONE simultaneous
solve over every active constraint, incl. cross-object glue_c
rand_mode(0) on pl → demotes the whole child to state for that call.Interview angle
What interviewers ask
“How does a non-rand variable behave inside a constraint?” — read as a constant at solve time; the solver constrains around it, never assigns it. The screening question.
“How do you parameterize transaction ranges per test without subclassing?” — non-rand knobs (or a cfg object handle) referenced in constraints; set procedurally, read at each solve.
“What changes when a member object handle is rand?” — its fields join the parent's solve; constraints spanning both become bidirectional, and parent randomize() recurses into it.
“Can a child constraint affect a parent field?” — yes, when the child is rand: one simultaneous system. Not when the child is plain state.
“What about null handles in constraints?” — dereferencing null at solve time is a runtime error; guard with handle != null -> ... implications.
Key takeaways
Non-rand variables in constraints are solve-time constants — the cheapest per-test control knob.
obj.field references read other objects as state, enabling config-driven and history-driven constraints.
rand object handles merge child variables into one simultaneous, bidirectional solve with the parent.
Guard cross-object dereferences with null checks inside implications.
rand_mode(0) on a rand handle demotes the entire child object to state for that call.
Common pitfalls
Forgetting to construct a rand child before randomize() — null dereference at solve time.
Setting knobs AFTER randomize() and wondering why the old range was used — knobs are read at the call.
Assuming a non-rand cfg field can be adjusted by the solver to avoid contradiction — state never moves.
Knob values that empty the solution space (max_len = 0) — randomize() returns 0, often blamed on the constraints.
Deep rand-object trees solved as one system — solver time explodes; demote stable subtrees with rand_mode(0).