Part 3 · Constraint Randomization · Intermediate
Layered Constraint Architecture
Base transaction with valid_c only, test layers adding policy via subclass or inline, and why base classes must not over-constrain.
The layering principle
Constraint inheritance has one rule with enormous architectural consequences: a subclass's constraints are ANDed with every inherited constraint (unless a block is overridden by name). A derived class can therefore only shrink the solution space, never grow it. This makes the base class a one-way door: anything you constrain there is constrained for every test, forever, unless tests resort to constraint_mode hacks. The discipline that follows: the base transaction carries only legality — what the protocol spec permits — and every narrower preference lives in a layer above it.
CONSTRAINT LAYERS AND WHO OWNS THEM
+--------------------------------------------------------+
| LAYER 3: TEST INTENT owner: test writer |
| class stress_txn extends bus_txn (subclass narrows) |
| assert(t.randomize() with { len > 12; }) (inline) |
| policy objects, knob settings |
| lifetime: one test |
+-----------------------------+----------------------------+
| AND
+-----------------------------v----------------------------+
| LAYER 2: TYPICALITY owner: VIP/env author |
| soft len inside {[1:4]}; (realistic defaults) |
| addr dist { low :/ 8, high :/ 2 }; |
| lifetime: project, overridable per test |
+-----------------------------+----------------------------+
| AND (soft yields to hard)
+-----------------------------v----------------------------+
| LAYER 1: LEGALITY owner: protocol spec |
| valid_c: what the SPEC permits, nothing narrower |
| lifetime: as long as the protocol - never disabled |
| except deliberately for error injection |
+----------------------------------------------------------+
Rule: information flows DOWN as AND; intent flows UP as
soft-override or subclass. Legality is never restated upstairs.Base class: legality only
class bus_txn;
rand bit [31:0] addr;
rand bit [3:0] len; // beats, 1..15 meaningful
rand bit [2:0] size; // bytes/beat = 2**size
rand bit write;
// LAYER 1 - legality: straight from the bus spec.
constraint valid_c {
len inside {[1:15]}; // spec: zero-beat illegal
size <= 3'd2; // spec: max 4 bytes/beat
addr % (1 << size) == 0; // spec: aligned to size
(addr[11:0] + (len << size)) <= 4096; // spec: no 4KB crossing
}
// LAYER 2 - typicality: soft, so any hard test constraint wins.
constraint typical_c {
soft len inside {[1:4]}; // most traffic is short
soft size == 3'd2; // word beats dominate
}
endclassNote what is absent: no “addr in the DDR window”, no “writes only”, no “len == 1 for bring-up”. Those are all real needs — of particular tests — and each belongs in layer 3. The soft keyword does the load-bearing work in layer 2: a soft constraint holds by default but is discarded (entirely, not relaxed) the moment any hard constraint conflicts, so tests override typicality without ceremony while legality stays inviolable.
Test layer: subclass and inline
// LAYER 3a - subclass: named, reusable test intent
class long_burst_txn extends bus_txn;
constraint intent_c { len inside {[12:15]}; }
// ANDed with valid_c: still aligned, still no 4KB cross.
// soft len inside {[1:4]} conflicts with a hard constraint
// -> discarded automatically. No constraint_mode needed.
endclass
class dma_window_txn extends bus_txn;
constraint window_c { addr inside {[32'h8000_0000:32'h8FFF_FFFF]}; }
endclass
// LAYER 3b - inline: one-off intent at the call site
bus_txn t = new();
assert(t.randomize() with { write == 1; len == 1; });
// LAYER 3c - override BY NAME: replace, not AND (the escape hatch)
class relaxed_txn extends bus_txn;
constraint typical_c { soft len inside {[1:15]}; }
// Same block name as the parent -> REPLACES parent's typical_c.
// Works because typicality is in its own named block, separate
// from valid_c. Naming discipline = override granularity.
endclassThe three mechanisms have distinct roles. Subclassing is for intent that recurs across tests — it is named, reviewable, and composable with factories. Inline with is for single-call-site intent — visible exactly where it applies. Named-block override is the surgical tool: because layers live in separately named blocks (valid_c, typical_c), a subclass can replace the typicality block wholesale without touching legality. This is why block naming is an architectural decision, not a style preference: the block is the unit of override and of constraint_mode control.
Why over-constrained base classes destroy testbenches
Consider the alternative: a well-meaning author adds len <= 4 to valid_c “because long bursts aren't used in this project.” Six months later a test must verify long-burst arbitration. The test writer's options are all bad: disable valid_c (losing alignment and 4KB legality with it — now generating illegal stimulus), edit the base class (perturbing every existing test and their closed coverage), or copy-paste a parallel transaction class (forking the codebase). The pattern generalizes:
Every constraint in a base class is a promise to every current and future test — narrow promises break first.
Hard constraints in the base cannot be escaped by subclasses (AND semantics) — only by disabling blocks, which throws away legality wholesale when legality and preference share a block.
The cheap insurance: anything narrower than the spec goes in as soft, in its own named block, or in a subclass — all three are escapable; a hard constraint in valid_c is not.
Symptom to watch for in review: constraint_mode(0) calls sprinkled through tests. That is tests fighting the base class — a layering failure, not a test bug.
Interview angle
Two reliable questions live here. “What happens to constraints under inheritance?” — they are ANDed; a same-named block overrides instead; so subclasses narrow, never widen. “How do you organize constraints in a reusable transaction?” — the three-layer answer: legality only in the base (hard, named valid_c), typicality as soft defaults in a separate named block, test intent via subclass/inline/policy. Strong candidates volunteer the failure mode: a hard non-spec constraint in the base forces tests into constraint_mode hacks that discard legality along with the unwanted preference. If asked “when is constraint_mode(0) on valid_c acceptable?” the answer is: deliberate error injection only, and even then prefer a partitioned design — covered in the error-injection lesson.
Key takeaways
Inheritance ANDs constraints; same-name blocks override — subclasses narrow, never widen.
Base transaction = legality only, verbatim from the spec, in a named valid_c block.
Typicality is soft and separately named — escapable by any hard test constraint, automatically.
Test intent lives in subclasses (recurring), inline with (one-off), or named-block overrides (surgical).
constraint_mode(0) scattered through tests is the smoking gun of an over-constrained base.
Common pitfalls
Putting project preferences (short bursts, address windows) into valid_c — future tests cannot escape.
Mixing legality and typicality in one block — disabling or overriding it throws away both.
Expecting a subclass constraint to widen a parent range — AND semantics make the result narrower or UNSAT.
Accidentally reusing a parent block name — silent override replaces inherited constraints entirely.
Restating legality in every subclass 'to be safe' — divergent copies rot as the spec evolves.