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.

systemverilog
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
end

Follow-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.

diagram
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 it

Follow-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.

systemverilog
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.
endclass

Follow-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.

systemverilog
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 }; }
endclass

Follow-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.

systemverilog
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 only

Follow-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.