Randomization is deterministic per tool: the same simulator version, seed, compile and testbench give the same values every run. That is what makes a failing seed reproducible.
The standard fixes which assignments are legal, how randc cycles, and how streams are scoped. It does not fix the generator, so the specific legal values, and their exact shares, belong to each tool.
Property | Same tool, same seed | Another seed | Another tool or version |
|---|---|---|---|
Every result satisfies the constraints | Yes | Yes | Yes |
The same sequence of values | Yes | No | No |
| Yes | Yes | Yes, in another order |
Shares match the standard's uniform rule | Only approximately | Only approximately | Only approximately, and differently |
Which calls fail on a borderline rule | Same | May differ | May differ |
Not necessarily. Both are correct when both results satisfy the constraints. Only legality and cycling travel, never the values.
This topic has collected the places where Verilator 5.048 and the standard disagree. Each one is legal behaviour a test could lean on by accident, and each one changes on the next tool. The full list is on What the Open Simulators Run.
Case | Standard | Verilator 5.048 |
|---|---|---|
| 2 of 12 | About 20% fewer |
Starved enable bit with | 50% | 26% |
Function call in a constraint | Arguments solved first; most calls fail | Pushed back; every call solved |
| Fails when 15 is dealt | Never dealt 15; never failed |
| Unsatisfiable | Solved |
Size first, with infeasible sizes in range | Can fail | Never failed |
Notice that Verilator is kinder in most rows. A bench developed only on Verilator can pass for months and then fail on a commercial simulator that follows the standard. The fix is the same each time: write the rule so that every tool agrees, and check properties, not values.
Random Stability and Seeding taught the hierarchy: each object gets its own generator, seeded from its thread when new() runs. The bench below measures the parts a test depends on: later activity leaves an object's stream alone, an earlier new() shifts it, and srandom and set_randstate re-anchor streams.
class txn;
rand bit [7:0] x;
endclass
module tb_stability;
function automatic string five(txn t); // the next five values of t
string s = "";
repeat (5) begin
if (!t.randomize()) $fatal(1, "FAIL: randomize failed");
s = {s, $sformatf("%0d ", t.x)};
end
return s;
endfunction
initial begin
std::process p;
txn a, b, d;
string ref_seq, s1, s2, s3, state;
bit [7:0] v1, v2;
int unsigned d1, d2;
p = std::process::self();
p.srandom(7); a = new(); ref_seq = five(a); // reference run
p.srandom(7); a = new(); void'($urandom); void'($urandom);
b = new(); void'(b.randomize()); s1 = five(a); // noise after a was built
p.srandom(7); a = new(); d = new(); void'(d.randomize());
s2 = five(a); // another object randomized
p.srandom(7); d = new(); a = new(); s3 = five(a); // one extra new() before a
$display("reference: %s", ref_seq);
$display("draws and new objects after a: %s", s1);
$display("another object randomized: %s", s2);
$display("one extra new() before a: %s", s3);
if (s1 != ref_seq || s2 != ref_seq) $fatal(1, "FAIL: a's stream moved");
if (s3 == ref_seq) $fatal(1, "FAIL: an earlier new() should shift a's seed");
b.srandom(32'hDEAD_BEEF); void'(b.randomize()); v1 = b.x;
b.srandom(32'hDEAD_BEEF); void'(b.randomize()); v2 = b.x;
$display("b.srandom twice with one seed: %0d and %0d", v1, v2);
if (v1 != v2) $fatal(1, "FAIL: srandom did not re-anchor b");
state = p.get_randstate(); // checkpoint the thread
d1 = $urandom;
p.set_randstate(state); // rewind it
d2 = $urandom;
$display("thread draw, then again after set_randstate: %0d and %0d", d1, d2);
if (d1 != d2) $fatal(1, "FAIL: set_randstate did not rewind the thread");
$display("PASS: a's stream ignores later activity, shifts with an earlier new(), and srandom and set_randstate re-anchor streams");
$finish;
end
endmodulereference: 236 191 232 245 251
draws and new objects after a: 236 191 232 245 251
another object randomized: 236 191 232 245 251
one extra new() before a: 254 126 31 66 124
b.srandom twice with one seed: 147 and 147
thread draw, then again after set_randstate: 1112668803 and 1112668803
PASS: a's stream ignores later activity, shifts with an earlier new(), and srandom and set_randstate re-anchor streamsThe bench reseeds the thread with p.srandom(7). On Verilator 5.048, once a design names std::process, $urandom(7) stopped reseeding what new() draws from, while p.srandom(7) kept working. The process form is the one the standard describes for this job, so use it.
Assert legality on every result, and assert properties such as “every length appeared” or “shares are ordered”.
Compare shares against a band, and print the measured numbers beside the expected ones.
Never store random values as expected data; store the seed and the tool version instead.
Log the seed in the first line of every run, and seed the whole hierarchy from that one value.
// snippet
int unsigned seed;
initial begin
if (!$value$plusargs("seed=%d", seed)) seed = 1;
$display("[REPRO] seed=%0d", seed); // the run script logs the tool and version
std::process::self().srandom(seed); // one anchor for everything built after it
endrandomize() fails on another. Who is wrong?Check whether the set is really satisfiable first. If it is, the second tool has a bug or a limit. If the rule is borderline, as with size-first or randc beside rand, the first tool was being kind, and the rule needs rewriting.
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 kept an object's stream fixed through later draws and other objects, and shifted it after an earlier new(). Once std::process appeared in the design, $urandom(7) stopped reseeding object construction while process::self().srandom(7) still did.