Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
    • SystemVerilog Data Types
      • 4-State vs 2-State Types
      • Nets vs Variables
      • Integer Types & Casting
      • Enums, Structs & Unions
      • X/Z Propagation & Pessimism
      • Strings, Reals & Special Types
    • Arrays: Packed, Dynamic, Queues & Associative
    • Procedural Code & Scheduling
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
    • Interfaces, Modports & Clocking Blocks
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹SystemVerilog Data Types
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. SystemVerilog Data Types
  5. X/Z Propagation & Pessimism

Part 1 · Language Foundations · Intermediate

X/Z Propagation & Pessimism

How X spreads through operators, pessimism vs optimism, === vs ==, X in if/case, and tracing X to its source in debug.

On this page

    How X moves through operators

    Once a bit is X, most operators propagate the uncertainty — but not blindly. Logic gates apply their controlling-value rules: X & 0 is 0 (a 0 input forces AND low regardless), X | 1 is 1, but X & 1 and X | 0 stay X. Arithmetic is harsher: one X bit anywhere makes the entire result X (8'h0X + 1 is all-X), because a carry chain can spread one unknown everywhere. Equality == returns X (not 0, not 1) if either operand contains X or Z — and an X condition in if is treated as false.

    Diagram
    Legend: [TYPE]
    
      X PROPAGATION RULES [TYPE]
    
      gates: controlling value wins        arithmetic: total loss
      ───────────────────────────          ──────────────────────
      X & 0 = 0    X | 1 = 1               4'b00X0 + 1 = 4'bXXXX
      X & 1 = X    X | 0 = X               X * anything = all X
      ~X    = X    X ^ a = X
                                           relational/equality:
      mux is the pessimism battleground    (a == b) with any X/Z → X
      ───────────────────────────────      if (X) → takes ELSE branch
      sel=X:  out = sel ? a : b
        LRM: if a==b bitwise, out=that bit (optimism!)
        if a!=b → X
      real gate: out could be a, b, or glitch — sim may be
      MORE optimistic than silicon (dangerous direction)

    This is where pessimism vs optimism comes in. Simulation is X-pessimistic when it reports X although real hardware would settle to a known value (e.g. two reconvergent paths of the same X that would cancel in silicon). It is X-optimistic when it reports a known value although silicon could differ — the dangerous direction. The classic optimism cases are if (x_cond) silently taking the else branch and casex/plain case matching despite X. Gate-level sim and formal X-prop tools exist largely to catch what RTL-level optimism hid.


    === vs == and writing X-aware checks

    == is the logical equality: any X/Z in either operand yields X, which an if then treats as false. === is case equality: it compares all four states literally, so 4'b10XZ === 4'b10XZ is 1 and the result is always 0 or 1, never X. Testbench checks should normally use == plus an explicit $isunknown guard — using === everywhere accidentally accepts X==X as a match, hiding the very bug you are hunting. There is also ==? (wildcard equality), where X/Z in the right-hand operand act as don't-care positions — useful for masked compares.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    logic [3:0] got, exp;
    
    // Scoreboard check: X-aware, X is a FAILURE not a match
    if ($isunknown(got))
      $error("DUT output contains X/Z: %b", got);
    else if (got !== exp)            // !== safe here: got proven known
      $error("mismatch got=%h exp=%h", got, exp);
    
    // Why plain == fails silently:
    got = 4'b1x00; exp = 4'b1100;
    if (got == exp)  $display("match");        // == is X → else branch
    else             $display("mismatch");     // prints, but for wrong reason
    if (got === exp) $display("exact match");  // 0: differs in bit 2
    
    // Wildcard compare: ignore don't-care bits of the EXPECTED value
    exp = 4'b1?00 ? 4'b1z00 : exp;             // conceptually
    if (got ==? 4'b1z00) $display("bit2 ignored in compare");

    Tracing X to its source

    X debug is a backwards walk: the X you see on an output is usually many cycles and modules downstream of its birth. The standard procedure — and a favorite interview scenario ("your scoreboard sees X at time 10us, what do you do?") — is: find the first time the signal goes X in the waveform, trace its fan-in cone at that time, and repeat upstream until you hit the origin. Origins are nearly always one of: an unreset flop, an unconnected/misconnected port (floats Z, then reads as X through logic), a bus conflict, an out-of-range index read, or a don't-care assignment (casez default X) escaping into live logic.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // X-source canaries you can build into the bench
    always @(posedge clk) begin
      if (!$isunknown(rst_n) && rst_n) begin     // only after clean reset
        if ($isunknown(dut_if.valid))
          $error("[%0t] valid is X — check reset of producer FSM", $time);
        if (dut_if.valid && $isunknown(dut_if.data))
          $error("[%0t] data X while valid high", $time);
      end
    end

    Why 2-state simulation hides these bugs

    • A 2-state (or X-suppressed) compile turns every would-be X into 0 — an unreset flop reads as 0, which often IS the reset value, so the missing reset is invisible.

    • The bug then escapes to gate-level netlist sim or silicon, where the flop genuinely powers up random.

    • Standard practice: keep RTL regressions 4-state, and treat any X after reset deassertion as an error.

    • Some flows deliberately randomize uninitialized state (2-state with random init) to compensate — know which mode your project runs.

    Key takeaways

    • Gates propagate X unless a controlling value decides; arithmetic and comparisons go fully X.

    • if/case are X-optimistic — X conditions silently take the else/default path, masking bugs.

    • Use == with an $isunknown guard in checkers; === everywhere makes X==X a false pass.

    • Debug X by walking the waveform back to the first X and its fan-in; origin is usually reset, a port, or a conflict.

    Common pitfalls

    • Using === in scoreboards 'to be safe' — X on both sides compares equal and the check passes.

    • Trusting a 2-state regression as proof of reset correctness — X-hiding converts unknowns to plausible 0s.

    • Forgetting if(X) takes the else branch — an X-valid signal makes the monitor silently drop transactions.

    • casex in RTL — X in the selector matches aggressively, an optimism trap; prefer case/unique case with explicit handling.

    Previous

    11 / 363

    Next: Strings, Reals & Special Types

    Up next

    Up nextStrings, Reals & Special Types

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • 4-State vs 2-State Types
    • Nets vs Variables
    • Integer Types & Casting