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.
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.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.
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.
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]};
}
endclassBoth 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.”
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.