Part 3 · Constraint Randomization · Intermediate

Constraint Override via Inheritance

Same-named constraint blocks replace the parent's, test-specific subclasses, factory substitution, and override vs constraint_mode.

Same name replaces; new name adds

Constraint blocks are inherited like methods, and like methods they are virtual by name : a subclass block with the same name completely replaces the parent's block for objects of the subclass — the parent's expressions in that block are gone, not merged. A subclass block with a new name simply adds another block that intersects with everything inherited. This name-based replace/add distinction is the entire mechanism, and the only relaxation tool besides constraint_mode.

systemverilog
class base_txn;
  rand bit [7:0] len;
  rand bit [3:0] prio;
  constraint len_c  { len inside {[1:16]}; }
  constraint prio_c { prio < 8; }
endclass

class jumbo_txn extends base_txn;
  // SAME NAME → REPLACES parent's len_c entirely (relaxation works!)
  constraint len_c { len inside {[1:255]}; }
endclass

class urgent_txn extends base_txn;
  // NEW NAME → ADDS; intersects with inherited len_c and prio_c
  constraint urgent_c { prio inside {[6:7]}; }
endclass

// jumbo_txn:  len ∈ [1:255]  (parent's [1:16] is GONE), prio < 8
// urgent_txn: len ∈ [1:16],  prio ∈ {6,7}  (prio<8 ∩ [6:7])

Because replacement is total, an overriding block must restate any parent expressions it still wants — a maintenance hazard when the parent block mixes several concerns. This is another argument for small single-purpose blocks: override granularity equals block granularity.


Test-specific transaction subclasses

The standard methodology pattern: the base transaction encodes protocol legality; each test (or test family) defines a thin subclass whose only job is to reshape distributions or relax/replace specific blocks. The subclass is a named, reviewable, reusable artifact — unlike a pile of inline with blocks scattered through sequences.

systemverilog
// Base: protocol legality only
class axi_txn;
  rand bit [31:0] addr;
  rand bit [7:0]  len;
  rand bit        write;
  constraint legal_c { len inside {[1:16]}; addr[1:0] == 0; }
  constraint shape_c { soft write dist { 1 :/ 1, 0 :/ 1 }; }
endclass

// Test personality 1: write-heavy small bursts near a hotspot
class hotspot_txn extends axi_txn;
  constraint shape_c {                       // replaces base shape_c
    write dist { 1 :/ 9, 0 :/ 1 };
    len inside {[1:4]};
    addr inside {[32'h8000:32'h80FF]};
  }
endclass

// Test personality 2: out-of-spec lengths for error checking
class badlen_txn extends axi_txn;
  constraint legal_c { addr[1:0] == 0; }     // replaces: len rule DROPPED
  constraint bad_c   { len inside {[17:64]}; }
endclass

Factory substitution: override without touching sequences

Subclass override becomes powerful at scale when combined with a factory (UVM's, or a hand-rolled one): sequences keep creating what they believe is the base type, and the factory returns the test's subclass instead. Every randomize() in every reused sequence now solves the subclass's constraint set — zero sequence edits.

systemverilog
// UVM flavor — in the test's build_phase:
//   axi_txn::type_id::set_type_override(hotspot_txn::get_type());
// Every sequence doing axi_txn::type_id::create("t") now gets hotspot_txn,
// and t.randomize() solves hotspot_txn's blocks (legal_c + new shape_c).

// The same idea without UVM — virtual "create" hook:
class txn_factory;
  static axi_txn proto = null;             // test installs a prototype
  static function axi_txn create();
    if (proto == null) begin axi_txn t = new(); return t; end
    return proto.clone_empty();            // returns subclass instance
  endfunction
endclass
diagram
FACTORY + CONSTRAINT OVERRIDE FLOW

  test build_phase:
     set_type_override(axi_txn  hotspot_txn)
                       │
  reusable sequence (UNCHANGED):
     t = axi_txn::type_id::create("t")  ──► actually hotspot_txn
     t.randomize()                      ──► solves:
                       │                      legal_c   (inherited)
                       │                      shape_c   (OVERRIDDEN ver.)
                       ▼
     one line in the test reshapes EVERY transaction in EVERY sequence

Override vs constraint_mode

  • Override is per-TYPE and compile-time-defined: every instance of the subclass behaves the new way; nothing to restore at runtime.

  • constraint_mode is per-INSTANCE and procedural: flexible mid-sequence, but sticky state someone must manage.

  • Override can REPLACE expressions (change a range); constraint_mode can only remove a whole block (all-or-nothing).

  • Override + factory scales to a whole environment with one line; constraint_mode requires a handle to every object you want to alter.

  • Use constraint_mode for temporary in-sequence toggles; use override for a test's standing policy.


Interview angle

What interviewers ask

  • “What happens when a subclass declares a constraint with the parent's block name?” — full replacement of that block for subclass objects, enabling relaxation. New names add and intersect.

  • “How do you change transaction constraints for one test without editing sequences?” — subclass with overriding blocks + factory type override; one build_phase line.

  • “Does the parent's replaced block still apply somehow?” — no; replacement is total, restate anything you still need.

  • “Override vs constraint_mode?” — type-level standing policy vs instance-level runtime toggle; replace-content vs remove-block.

  • “Why keep constraint blocks small?” — override granularity equals block granularity; big blocks force overrides to restate unrelated rules.

Key takeaways

  • Same-named subclass constraint blocks replace the parent's entirely; new names add and intersect.

  • Override is the clean relaxation mechanism — the parent's expressions in that block cease to exist.

  • Test-specific subclasses make stimulus personalities named, reviewable, and reusable.

  • Factory substitution applies an override across every sequence with one line and zero sequence edits.

  • Keep blocks small and single-purpose — override granularity equals block granularity.

Common pitfalls

  • Expecting parent + child same-named blocks to merge — replacement is total, not additive.

  • Overriding a multi-concern block and silently dropping rules you forgot to restate.

  • Renaming a parent block and breaking every subclass override bound to the old name — the link is purely the string name.

  • Building per-test personalities as inline with piles instead of subclasses — unreviewable, unreusable.

  • Forgetting that randomize() is not virtual-dispatch magic: it is the object's TYPE (set at creation/factory) that decides which blocks exist.