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.

diagram
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

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

Note 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

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

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