Part 3 · Constraint Randomization · Intermediate
Q&A: Solver Semantics
Bidirectional solving, seed stability across vendors, randc-rand interaction, distribution skew from implications, and how state variables are treated.
Q: Is constraint solving bidirectional?
Direct answer: yes. All active constraints and all rand variables form one simultaneous satisfaction problem — there is no dataflow direction. Writing total == a + b does not “compute” total from a and b; pin total inline and the solver pushes values back into a and b just as happily. This is the single most important conceptual difference from procedural code, and half the other interview questions are this fact in disguise.
class pkt;
rand int unsigned hdr, pay, total;
constraint c { hdr inside {[4:16]};
pay <= 1024;
total == hdr + pay; }
endclass
initial begin
pkt p = new();
void'(p.randomize()); // forward: total derived
void'(p.randomize() with { total == 64; }); // backward: hdr+pay decomposed
endFollow-up you should expect
“If it’s bidirectional, what does solve...before do?” — It biases the probability by partitioning the solve into stages; the constraint graph stays bidirectional within and across stages, and legality is untouched. The pair of facts — simultaneous legality, stage-able probability — is the complete senior model.
Junior vs senior answer
Junior: “the solver figures out all the variables together.” Vague but pointed the right way.
Senior: demonstrates a backward solve (pinning the “output”), and reconciles bidirectionality with solve...before in one sentence.
Q: Same seed, same results — within one simulator? Across vendors?
Direct answer: within one simulator version, yes — SystemVerilog defines random stability : each thread and each object gets its own RNG seeded hierarchically, so the same seed plus the same code reproduces the same values. Across vendors, no — the LRM pins the stability model , not the solver’s solution-picking algorithm, so VCS, Xcelium, and Questa legally produce different value streams from identical seeds. Even within one vendor, upgrading versions can shift streams.
Legend: [QA]
RANDOM STABILITY — WHAT IS AND ISN'T GUARANTEED [QA]
same simulator + same version + same seed + same code
→ SAME values (this is the repro contract)
same seed, DIFFERENT vendor → different values (legal)
same seed, different simulator VERSION → may differ (solver changed)
same seed, new object inserted earlier → downstream objects shift
(object seeding is hierarchical/order-sensitive)
Practical rules:
- log the seed of every run; rerun failures with that seed
- don't condition test logic on exact random values
- adding a $urandom call upstream can shift everything after itFollow-up you should expect
“Why did my failure stop reproducing after an unrelated edit?” — Random stability is hierarchical and order-sensitive: constructing one extra object or adding one $urandom call upstream re-seeds everything created after it. That is why reproduction discipline pairs the seed with the exact code revision — seed alone is half the key.
Junior vs senior answer
Junior: “same seed gives same results.” — true only with the qualifiers.
Senior: scopes the guarantee (simulator+version+code), denies cross-vendor portability, and explains hierarchical seeding’s sensitivity to construction order.
Q: How does randc interact with rand in the same solve?
Direct answer: randc variables are solved first — the cycling permutation proposes the randc value, and the rand variables are then solved subject to it. Consequences: you cannot write solve <rand> before <randc> (the implicit order already puts randc first); a constraint between a randc and a rand variable effectively conscripts the rand variable to follow the cycle; and if constraints reject the value the cycle proposes, the solve can fail even though other randc values would have worked.
class lockstep;
randc bit [3:0] idx; // cycles 0..15 in permuted order
rand bit [7:0] data;
constraint c { data == idx * 10; } // data is dragged along the cycle
endclass
class risky;
randc bit [3:0] sel;
rand bit [3:0] other;
constraint c { sel + other < 6; }
// When the cycle proposes sel = 14, no legal 'other' exists →
// randomize() returns 0 mid-cycle. randc + tight constraints = fragile.
endclassFollow-up you should expect
“So when is randc actually appropriate?” — When you want exhaustive-without-repeat over a lightly constrained small space: sweeping opcodes, register indices, port numbers. The moment heavy cross-constraints arrive, prefer a rand variable with uniqueness or history constraints, or pre-shuffle a list procedurally — the cycle’s rigidity becomes a liability.
Junior vs senior answer
Junior: “they work together fine; randc just doesn’t repeat.”
Senior: states the randc-first ordering, the dragging effect on constrained rand variables, the mid-cycle failure mode, and the design guidance to keep randc lightly constrained.
Q: Why did my distribution skew when I added an implication?
Direct answer: because the default solver is uniform over legal solution tuples , not over each variable. An implication typically makes one arm’s solution count much larger than the other’s — and the variable in the antecedent inherits the imbalance. Add err -> code inside {[1:255]} with !err -> code == 0 and there are 255 solutions with err=1 against 1 with err=0: err is now true 255 out of 256 solves, even though nothing constrains err directly.
class pkt;
rand bit err;
rand bit [7:0] code;
constraint c_arm { err -> code inside {[1:255]};
!err -> code == 0; }
// Fix A: stage the solve — err uniform first, code within the arm
constraint c_fix { solve err before code; }
// Fix B: pin the antecedent's distribution explicitly
// constraint c_dist { err dist { 0 :/ 1, 1 :/ 1 }; }
endclassFollow-up you should expect
“How would you have CAUGHT this?” — Functional coverage on err, or a quick histogram loop in a unit test of the transaction class. The senior habit: after writing any implication, ask “how many solutions does each arm have?” — arm-size imbalance is distribution skew, every time. This is also why transaction classes deserve their own randomization unit tests before they ever meet a DUT.
Junior vs senior answer
Junior: “the solver is biased, add solve-before.” — right knob, wrong diagnosis; the solver is perfectly uniform, just over tuples.
Senior: explains uniform-over-tuples as the mechanism, counts the arms to predict the skew, then offers both fixes and the detection method.
Q: How does the solver treat state (non-rand) variables?
Direct answer: as constants sampled at the moment randomize() is called . Any constraint mentioning them is still fully active — it just has those terms fixed. This is the standard mechanism for test-configurable randomization: the test writes knobs (modes, bounds, enables) into state variables, and the constraints reshape around them with no solver-control calls at all. A rand variable with rand_mode(0) behaves identically for that call.
class txn;
rand bit [31:0] addr;
bit [31:0] region_lo, region_hi; // state knobs — test writes these
bit excl_en;
bit [31:0] excl_lo, excl_hi;
constraint c_region { addr inside {[region_lo:region_hi]}; }
constraint c_excl { excl_en -> !(addr inside {[excl_lo:excl_hi]}); }
endclass
// Test:
// t.region_lo = 32'h8000; t.region_hi = 32'hFFFF;
// t.excl_en = 1; t.excl_lo = 32'hA000; t.excl_hi = 32'hAFFF;
// void'(t.randomize()); // solver sees fixed bounds, solves addr onlyFollow-up you should expect
“What if the test sets region_lo > region_hi?” — The range is empty, the constraint is unsatisfiable, randomize() returns 0 at runtime — no compile or elaboration check exists for state-variable sanity. Knob-driven classes therefore want either assertions on the knobs or a checked randomize that fatals with the knob values in the message; otherwise an env misconfiguration surfaces as a mysterious mid-regression solve failure.
Junior vs senior answer
Junior: “non-rand variables aren’t randomized.” — true, half the story.
Senior: “read as constants at call time, constraints stay active around them” — then connects it to the knob pattern and the runtime-only failure mode of bad knob values.
Key takeaways
One simultaneous problem, no direction — pin any variable and the rest re-solve around it.
Seed repro holds per simulator+version+code; never across vendors; construction order shifts streams.
randc solves first and drags constrained rand variables along its cycle — keep randc lightly constrained.
Uniform-over-tuples explains every implication skew; count arm solutions to predict it.
State variables are call-time constants — the knob pattern, with runtime-only failure on bad knobs.
Common pitfalls
Reasoning about constraints as forward dataflow — bidirectionality breaks every such prediction.
Promising cross-vendor reproducibility from a seed — the LRM does not guarantee it.
Heavy cross-constraints on randc — mid-cycle infeasibility that intermittently fails regressions.
Diagnosing implication skew as “solver bias” — the solver is uniform; the solution space isn’t.