Part 3 · Constraint Randomization · Intermediate

Implication -> and if/else Constraints

Truth-table reasoning for a -> b, bidirectional solving, if/else equivalence, and chained implications.

Implication is a boolean expression, not a procedural if

a -> b reads “if a holds, then b must hold.” Formally it is the boolean (!a) || b, and that formula is the key to every interview question about it. The constraint is satisfied in three of the four truth-table rows — including both rows where a is false. So when a is false, b is completely unconstrained by this expression: the solver may pick any value for the variables in b, because the implication is already satisfied.

diagram
TRUTH TABLE for  a -> b   (equivalent to !a || b)

    a      b      a -> b satisfied?
  ─────  ─────  ───────────────────
  false  false       YES   ← b unconstrained when a false
  false  true        YES   ← b unconstrained when a false
  true   false       NO    ← the only forbidden row
  true   true        YES

  The constraint only FORBIDS (a && !b).
  It says nothing about b when a is false,
  and nothing about a when b is true.
systemverilog
class dma_txn;
  rand bit       small_mode;
  rand bit [7:0] len;

  constraint len_c { small_mode -> len <= 8; }
endclass
// small_mode==1 → len ∈ [0:8]
// small_mode==0 → len ∈ [0:255]  (full range — NOT "len > 8")

The classic wrong answer is “when small_mode is 0, len is greater than 8.” No — the implication imposes nothing in that case. If you want both directions, you must write both, or use if/else.


Bidirectional solving — the solver can pick a to satisfy b

Constraints are not assignments and have no left-to-right direction. The solver sees a -> b as one relation over all the variables in both a and b, and solves them simultaneously . A startling consequence: a constraint that looks like “a controls b” also lets b's feasibility influence a.

systemverilog
class bi_demo;
  rand bit       flag;
  rand bit [3:0] val;

  constraint c1 { flag -> val == 9; }
  constraint c2 { val inside {[0:7]}; }   // 9 is impossible!
endclass
// Solver reasoning (simultaneous):
//   flag==1 requires val==9, but c2 forbids 9 → no solution with flag==1
//   Therefore EVERY solution has flag==0.
//   P(flag==1) = 0 — the "consequent" constrained the "antecedent".
// randomize() still SUCCEEDS (solutions exist), but flag is forced to 0.

This bidirectionality is also why P(flag) gets skewed even without contradictions: the solver is uniform over (flag, val) pairs , not over flag first. With c1 alone, flag==1 pairs with exactly one val (9) while flag==0 pairs with sixteen — so P(flag==1) = 1/17, not 1/2. Fixing that skew is exactly what solve...before is for, covered in the next sub-lesson.


if/else — implication with an else arm

An if/else constraint is sugar for a pair of implications: if (a) b; else c; is exactly (a -> b) && (!a -> c). Use it when you genuinely want both branches constrained; use bare -> when the false branch should remain free.

systemverilog
class pkt;
  rand bit       jumbo;
  rand bit [13:0] len;

  // Both branches pinned down — no unconstrained hole:
  constraint len_c {
    if (jumbo) len inside {[1501:9000]};
    else       len inside {[64:1500]};
  }

  // Identical meaning written as two implications:
  constraint len_c2 {
    jumbo  -> len inside {[1501:9000]};
    !jumbo -> len inside {[64:1500]};
  }
endclass

Both forms remain bidirectional: if a test later adds len == 5000 inline, the solver is forced to choose jumbo==1. The condition is solved, not evaluated.


Chained and nested implications

Implications compose. A chain a -> (b -> c) is again just boolean algebra: it equals (a && b) -> c. Nesting if/else builds mode trees, which is how real transactions encode “mode selects sub-mode selects field range.”

systemverilog
class mem_txn;
  typedef enum {READ, WRITE, ATOMIC} op_e;
  rand op_e      op;
  rand bit [3:0] burst;
  rand bit       locked;

  constraint shape_c {
    // Atomic ops are single-beat and locked; others free unless...
    op == ATOMIC -> (burst == 1 && locked == 1);
    // ...writes longer than 8 beats must not be locked:
    (op == WRITE && burst > 8) -> locked == 0;
  }
endclass
// Note (op==WRITE && burst>8) -> locked==0 is the flattened form of
// op==WRITE -> (burst>8 -> locked==0) — identical solution set.

Interview angle

What interviewers ask

  • “What happens to b when a is false?” — b is unconstrained by that implication; a -> b is just !a || b. This is THE screening question for this topic.

  • “Is a -> b the same as if (a) b;?” — yes, exactly; if/else adds the !a -> c arm.

  • “Can a constraint on the right side affect the left side variable?” — yes; solving is simultaneous and bidirectional. Give the flag/val example where flag is forced to 0.

  • “Why is P(flag) not 50/50 under flag -> val==9?” — uniformity is over solution pairs: 1 pair with flag==1 vs 16 with flag==0 → 1/17. Then name solve...before as the fix.

Key takeaways

  • a -> b means !a || b — the only forbidden combination is a true with b false.

  • When a is false, the implication says nothing about b; write if/else to constrain both branches.

  • Constraints solve simultaneously and bidirectionally — the consequent can force the antecedent.

  • Uniformity is over complete solutions, so implications skew the antecedent's distribution.

  • if (a) b; else c; is sugar for (a -> b) && (!a -> c).

Common pitfalls

  • Reading -> as procedural: assuming b gets a “default” or opposite constraint when a is false.

  • Expecting flag to be 50/50 when its implication consequent has few solutions — pair-counting decides.

  • Forgetting an impossible consequent silently forces the antecedent false instead of failing randomize().

  • Writing a -> b; when the spec needs both directions — missing the !a branch leaves a hole tests will hit.

  • Confusing -> (implication) with <-> (equivalence, a iff b) — equivalence constrains both rows.