Part 3 · Constraint Randomization · Intermediate

Bidirectional Solving

Constraints as equations not assignments: implications constrain both sides, the (x==0)->(y==0) joint distribution worked out, and the solve-before comparison.

Implication is a filter, not an if-statement

The constraint a -> b reads like procedural code ("if a then make b true") but means something different: it removes every tuple where a is true and b is false . Logically it is !a || b. Crucially, the surviving tuples include ones where a is false — and if making b true is impossible or rare, the solver will prefer tuples where a is false . The implication has constrained its own antecedent. That is bidirectionality: every constraint shapes every variable it mentions, in both directions.

systemverilog
class backward;
  rand bit [3:0] a;
  rand bit [3:0] b;
  constraint c {
    (a == 5) -> (b == 100);   // b is 4 bits: b==100 is IMPOSSIBLE
  }
endclass
// Procedural reading: "a can be anything; if it lands on 5, set b=100."
// Solver reality: tuples with a==5 require b==100, which no 4-bit b
// satisfies -> ALL a==5 tuples are removed.
// Result: a is NEVER 5. The implication constrained a backwards.

This backward pressure is the single most-tested solver behavior in interviews, usually via the joint-distribution question worked out below.


The classic: (x==0) -> (y==0) joint distribution

One rand bit x, one rand bit y, single constraint (x==0) -> (y==0). Question: what is the probability of each (x, y) combination? Enumerate the four tuples and filter:

diagram
JOINT DISTRIBUTION OF  (x==0) -> (y==0)    [== !(x==0) || (y==0)]

   x  y | implication check          | legal? | probability
  ------+-----------------------------+--------+------------
   0  0 | antecedent T, consequent T  |  yes   |   1/3
   0  1 | antecedent T, consequent F  |  NO    |    0
   1  0 | antecedent F  (vacuous)     |  yes   |   1/3
   1  1 | antecedent F  (vacuous)     |  yes   |   1/3

  Marginals:  P(x=0) = 1/3        P(y=0) = 2/3
              P(x=1) = 2/3        P(y=1) = 1/3

  Procedural intuition said: "x is a fair coin (1/2), and y only
  gets forced when x==0."  WRONG: x is biased to 1, because x==0
  survives in only one tuple while x==1 survives in two.

The marginal P(x=0) = 1/3 — not 1/2 — is the punchline. The implication reduced the number of tuples in which x==0 can appear, so the uniform-over-tuples rule shifts probability toward x==1. No dist, no ordering — just set filtering.

systemverilog
module prove_it;
  class xy;
    rand bit x, y;
    constraint c { (x == 0) -> (y == 0); }
  endclass
  initial begin
    xy o = new();
    int n00, n10, n11;
    repeat (30000) begin
      void'(o.randomize());
      case ({o.x, o.y})
        2'b00: n00++;
        2'b10: n10++;
        2'b11: n11++;
        default: $fatal(1, "illegal tuple 01 generated!");
      endcase
    end
    $display("00:%0d 10:%0d 11:%0d", n00, n10, n11);
    // expect roughly 10000 / 10000 / 10000  (1/3 each)
  end
endmodule

Restoring the procedural intent with solve...before

If you wanted the procedural behavior — x a fair coin, y reacting — solve x before y delivers it. Ordering splits the solve into stages: first pick x uniformly over the values of x that admit at least one legal completion, then pick y uniformly among values legal given that x. The solution set is unchanged — tuple (0,1) is still illegal — but the probability mass moves.

systemverilog
class xy_ordered;
  rand bit x, y;
  constraint c   { (x == 0) -> (y == 0); }
  constraint ord { solve x before y; }
endclass
diagram
SAME CONSTRAINT, WITH vs WITHOUT ORDERING

  no ordering (uniform tuples)      solve x before y (staged)
  ----------------------------      --------------------------
  P(0,0) = 1/3                      stage 1: x in {0,1}, both
  P(1,0) = 1/3                               viable -> 1/2 each
  P(1,1) = 1/3                      stage 2: x=0 -> y must be 0
                                             x=1 -> y in {0,1}, 1/2 each
  P(x=0) = 1/3                      P(0,0) = 1/2
  P(x=1) = 2/3                      P(1,0) = 1/4
                                    P(1,1) = 1/4
                                    P(x=0) = 1/2   <- fair coin restored

  Legal tuples IDENTICAL in both columns.
  Only the WEIGHTS changed. Ordering never adds or
  removes solutions — it re-distributes probability.

Memorize both columns of that table. "1/3-1/3-1/3 without ordering, 1/2-1/4-1/4 with solve-before" is a complete senior-level answer to the most common solver interview question asked today.


Interview angle

What interviewers probe

  • "(x==0)->(y==0): what is P(x=0)?" — expected: 1/3. Enumerate the three legal tuples; uniform over tuples biases x toward 1.

  • "How do you make x a fair coin again?" — expected: solve x before y; staged solve gives 1/2, 1/4, 1/4.

  • "Does solve-before change which values are legal?" — expected: never; it re-weights, the solution set is identical.

  • "Constraint (a==5)->(b==100) with 4-bit b — what values can a take?" — expected: anything except 5; impossible consequents eliminate their antecedents backwards.

  • "Is a -> b the same as if (a) b inside constraints?" — expected: the if/else constraint form and -> are equivalent filters; neither is procedural.

Strong candidates volunteer the equivalence a -> b == !a || b early — it signals you see constraints as Boolean algebra over the tuple space, which makes every backward-pressure question trivial.

Key takeaways

  • a -> b is the filter !a || b — it removes (a true, b false) tuples and pressures a toward false when b is hard.

  • (x==0)->(y==0) yields P(x=0)=1/3 under default uniform-tuple semantics — not 1/2.

  • solve x before y restores procedural intuition: x picked first among viable values, then y given x.

  • Ordering re-weights probability only; the legal solution set is bit-for-bit identical.

  • Impossible consequents eliminate their antecedent values entirely — backward constraint propagation.

Common pitfalls

  • Reading -> as a procedural if — predicting fair-coin marginals that the solver does not produce.

  • Using solve-before hoping to FIX an unsatisfiable set — ordering never adds solutions; contradiction stays contradiction.

  • Writing (a==5)->(b==100) style constraints with unreachable consequents — silently bans a==5 and nobody notices.

  • Chaining implications both directions (a->b and b->a) without realizing you built an equality filter.

  • Forgetting that if/else inside constraints is also declarative — both branches shape the space, no execution order.