Part 3 · Constraint Randomization · Intermediate
Arrays of rand Objects
Construct-then-randomize for object arrays, constraints across elements, and modeling transaction sequences with gap and back-to-back constraints.
The solver randomizes fields, never allocates handles
Declaring rand txn seq[] makes the array of handles random in size, and recursively randomizes the rand fields of every non-null object the handles point to. What randomize() will never do is call new(): a null handle stays null and its "fields" simply do not exist in the constraint set. The mandatory pattern is therefore construct first, randomize second — size the array, new() every element, then call randomize() once on the container.
class txn;
rand bit [31:0] addr;
rand bit [7:0] len;
rand bit is_write;
constraint c_len { len inside {[1:16]}; }
endclass
class burst;
rand txn seq[];
// Size choice is made BEFORE solving, in pre_randomize,
// because we must allocate objects before the solver runs.
function void pre_randomize();
int n = $urandom_range(4, 8);
seq = new[n];
foreach (seq[i]) seq[i] = new();
endfunction
endclass
module top;
initial begin
burst b = new();
void'(b.randomize()); // solves every seq[i].addr/len/is_write
foreach (b.seq[i])
$display("[%0d] %s addr=%h len=%0d",
i, b.seq[i].is_write ? "WR" : "RD",
b.seq[i].addr, b.seq[i].len);
end
endmoduleNote the trade-off: because objects must exist before solving, the number of objects cannot be a solver decision the way a scalar array's size can. Pick the count procedurally (pre_randomize or test code), or over-allocate and use a rand "valid count" field that constraints reference.
Constraints across object array elements
Once the objects exist, the container's constraint block can reference seq[i].field inside foreach — the solver flattens all objects' rand fields into one constraint problem, so cross-object relations (ascending addresses, alternating direction, no two consecutive writes) are just element constraints over fields.
class ordered_burst;
rand txn seq[];
function void pre_randomize();
seq = new[6];
foreach (seq[i]) seq[i] = new();
endfunction
constraint c_relations {
// ascending, non-overlapping address windows
foreach (seq[i])
if (i > 0)
seq[i].addr >= seq[i-1].addr + (seq[i-1].len * 4);
// first transaction is always a write
seq[0].is_write == 1;
// no two consecutive reads
foreach (seq[i])
if (i > 0)
!(seq[i].is_write == 0 && seq[i-1].is_write == 0);
}
endclassEverything solves in one call : each object's own constraints (like c_len in txn) and the container's cross-object constraints form a single joint problem. This is what distinguishes one-call container randomization from a loop of per-object seq[i].randomize() calls — the loop cannot enforce relations that look forward (e.g. "sum of all lens == 64"), only backward-looking ones against already-solved items.
ONE-CALL vs PER-ELEMENT RANDOMIZATION
container.randomize() foreach loop of seq[i].randomize()
------------------------- ---------------------------------
all objects' fields in ONE each object solved ALONE,
joint constraint set earlier results frozen as state
can enforce: can enforce:
- sum of lens == 64 - seq[i] vs seq[i-1] (backward,
- global ordering passed via constraint args or
- "exactly 2 writes" state variables)
cannot enforce:
- totals over future elements
- global counts / budgets
cost: one big solve cost: N small solves (often faster,
(can be slow for big N) weaker expressiveness)Modeling transaction sequences: gaps and back-to-back
A common stimulus model adds a rand inter-transaction gap (idle cycles before each item) to the transaction or the container. Constraining gaps lets one knob sweep traffic density: all-zero gaps give back-to-back stress, bounded gaps give throttled traffic, and a mostly-zero dist gives bursty realistic patterns.
class spaced_burst;
rand txn seq[];
rand bit [7:0] gap[]; // idle cycles BEFORE seq[i]
function void pre_randomize();
seq = new[8];
foreach (seq[i]) seq[i] = new();
endfunction
constraint c_shape {
gap.size() == seq.size();
gap[0] == 0; // start immediately
// bursty profile: mostly back-to-back, occasional long gap
foreach (gap[i])
if (i > 0)
gap[i] dist { 0 := 70, [1:3] := 25, [10:20] := 5 };
// at least one TRUE back-to-back pair somewhere
gap.sum() with (int'(item.index > 0 && item == 0)) >= 1;
}
endclass
// driver consumes: repeat (gap[i]) @(posedge clk); drive(seq[i]);The counting-sum at the end reuses the sum-of-booleans idiom from the aggregates lesson to guarantee the interesting corner (a real back-to-back pair) appears in every generated burst rather than hoping the distribution produces one.
Interview angle
What interviewers probe
"You declared rand txn seq[10] and randomize() — what happens?" — expected: null-handle awareness; nothing is randomized (or a runtime error on access) until every element is new()'d. Construct-then-randomize.
"Why can't the solver choose how many objects to create?" — expected: randomize() never calls new(); object count is procedural. Mention the over-allocate + rand valid-count workaround.
"Constrain transaction N against transaction N-1" — expected: container-level foreach over seq[i].field with an i>0 guard.
"One randomize on the container vs a loop of per-item randomizes?" — expected: joint solve enables global/forward constraints (sums, counts); the loop is cheaper but only supports backward-looking relations.
The construct-before-randomize rule is one of the most common real-bug interview questions because the failure mode is silent in some tools: randomize() succeeds, the handles stay null, and the testbench crashes later at first dereference.
Key takeaways
randomize() recursively solves rand fields of non-null objects — it never constructs; new() every element first.
Object count is a procedural decision (pre_randomize or test code), not a solver decision.
Container-level foreach over seq[i].field expresses cross-object relations, solved jointly with each object's own constraints.
One-call container randomization supports global budgets and forward-looking relations; per-element loops do not.
A rand gap[] array beside the transaction array is the standard knob for back-to-back vs throttled vs bursty traffic.
Common pitfalls
Calling randomize() on an array of null handles — silently nothing solved, null-deref later; always new() in pre_randomize or before.
Allocating objects with seq = new[N] but forgetting the per-element seq[i] = new() loop — new[N] creates null handles only.
Expecting seq.size() to be solver-chosen like a scalar array — object existence precedes solving; pick count procedurally.
Re-using one object for every slot (seq[i] = shared_h) — all slots alias one object; each needs its own new().
Enforcing a global budget (total len) with a per-element randomize loop — impossible; switch to one-call container randomization.