Declaring rand txn seq[] lets randomize() solve the rand fields of every non-null object in the array. A null handle stays null, and nothing of it enters the solve.
So the pattern is build first, randomize second: size the array, new() every element, then call randomize(). The object count is procedural, never a solver choice.
// snippet
class burst;
rand txn seq[];
function void pre_randomize();
seq = new[4]; // 4 null handles
foreach (seq[i]) seq[i] = new(); // now 4 objects
endfunction
constraint c_rel {
seq[0].is_write == 1; // first is a write
foreach (seq[i]) if (i > 0)
longint'(seq[i].addr) >= longint'(seq[i-1].addr) + seq[i-1].len * 4;
}
endclassThis is the standard form, and it is how a commercial simulator runs a burst. Verilator 5.048 ignored each element's own constraints in an array of rand handles, so the fragment above is not run here. The loop form below runs everywhere.
randomize() runs on an array of null handles. What is solved?Nothing in them. Null handles have no fields in the solve, so every element needs its own new() first.
A loop of seq[i].randomize() calls solves each object alone, with earlier objects already fixed. Backward rules, such as a destination different from the previous one, go in each call's with clause using local:: to name the previous object.
Rule | One call | Loop |
|---|---|---|
| Yes | Yes, via |
The first packet is DATA | Yes | Yes, in the first call |
Lengths total exactly 32 | Yes | No: the last packet cannot fix an earlier one |
Exactly two ERR packets | Yes | Only with a count kept by hand |
Each call solves one object with the past fixed. Only one joint solve sees every length at once, and the loop has no such solve.
A rand gap array beside the packets sets idle cycles before each one. All-zero gaps stress back-to-back traffic, bounded gaps throttle, and a mostly-zero dist gives bursty patterns.
A guaranteed back-to-back pair needs care. A counting sum() over the same elements as the dist made about a third of Verilator's calls fail, though legal answers existed. A rand index b2b that forces one gap to zero, with the dist guarded off that slot, works.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
class pkt_txn;
rand kind_e kind;
rand bit [1:0] dest;
rand bit [3:0] len;
constraint legal_c { len inside {[1:8]}; dest inside {[0:2]}; }
endclass
class pkt_burst;
pkt_txn seq[]; // built here, solved one by one
rand bit [7:0] gap[8]; // idle cycles before seq[i]
rand bit [2:0] b2b; // where the guaranteed pair sits
int n;
constraint c_shape {
gap[0] == 0; // start at once
foreach (gap[i])
if (i > 0 && i != b2b) gap[i] dist { 0 :/ 70, [1:3] :/ 25, [10:20] :/ 5 };
b2b inside {[1:7]}; // one slot forced back-to-back
foreach (gap[i]) if (i == b2b) gap[i] == 0;
}
function void build(int count);
n = count;
seq = new[n];
foreach (seq[i]) seq[i] = new(); // randomize() never calls new()
endfunction
function bit fill();
if (randomize() == 0) return 0; // the gap array, in one solve
foreach (seq[i]) begin
if (i == 0) begin
if (!seq[i].randomize() with { kind == DATA; }) return 0;
end else begin
if (!seq[i].randomize() with { dest != local::seq[i-1].dest; }) return 0;
end
end
return 1;
endfunction
endclass
gaps after the first: 70% zero, 60 long ones in 1400
last burst gaps: 0 0 0 0 0 0 0 0
PASS: 200 bursts open with DATA, never repeat a dest, and always hold a back-to-back pairCheck the measured shares against the arithmetic. Six of the seven later gaps follow the dist, so long gaps should be 6 × 5% out of 7, about 4.3%, and Verilator gave 4.3%. Zeros should be 6 × 70% plus the forced one, out of 7, about 74%; Verilator gave about 70%, a small drift like the one in dist: Weighted Distributions. The driver then waits gap[i] cycles before sending seq[i].
Procedurally: pick the count in the test or in pre_randomize, or build the most you need and gate them with a rand valid count.
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 ignored element constraints in arrays of rand handles, so the PacketFIFO burst uses the loop form. A dist plus a counting sum on the same gaps failed about a third of calls; the bench forces one gap with a rand index and keeps the dist off it.