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. 4-State vs 2-State Types

Part 1 · Language Foundations · Intermediate

4-State vs 2-State Types

Four-state and two-state types, X propagation, X-aware comparisons, and finding X sources.

On this page

    Why this matters

    A test passed for weeks while the DUT drove X on its address bus. The testbench copied the bus into an int, the X became 0, and 0 was an address the check accepted.

    After this lesson you can: choose 4-state or 2-state for a signal, predict how X spreads through logic and arithmetic, and guard the copy from DUT pins into 2-state variables.

    What four states buy

    A 4-state type (logic, reg, wire, integer) stores each bit as 0, 1, X, or Z. X means unknown and Z means undriven, so simulation can show an uninitialized flop or a floating bus directly.

    A 2-state type (bit, byte, shortint, int, longint) stores only 0 or 1. Each 4-state bit costs two host bits (a value plane plus an X/Z plane) while a 2-state bit costs one, so large testbench structures roughly halve memory with 2-state types.

    Initialization differs in a way that matters for debug. A 4-state variable starts at X, which marks a missing assignment plainly, while a 2-state variable starts at 0, which looks like a legal value.

    Four states cost two host bits per bit and start at X; two states cost one and start at 0.

    Where each family belongs

    Anything that touches the DUT stays 4-state, and variables used only inside the testbench can go 2-state. RTL needs X so that reset bugs and bus conflicts stay visible, while a testbench counter or an address the testbench chose itself should never hold X.

    The driving direction is safe: every 2-state value is already a valid 4-state value. Only the sampling direction needs a guard, which is the next section.

    dut_side.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: the placement rule in each world
    module dut_side (input logic [7:0] d);   // RTL pins: 4-state keeps reset bugs visible
    endmodule
    
    module tb_side;
      bit [31:0]   addr;                      // testbench-only values: 2-state is cheaper
      int unsigned delay;
    endmodule

    Conversion at the boundary

    Assigning 4-state to 2-state silently maps X and Z to 0. That is the single most dangerous implicit conversion in the language: testbench code copies an X-laden bus into a bit [31:0] variable, and the check compares clean-looking garbage.

    Sample DUT pins into 4-state variables first and check them with $isunknown before copying to 2-state ones. If a mismatch shows a suspicious 0, ask whether it was born as X upstream.

    design.sv
    // snippet: guard partial X (for example 4'b1x00) before the 2-state copy
    if ($isunknown(raw_addr)) $display("X/Z on addr: %h", raw_addr);
    tb_addr = raw_addr;   // the 2-state copy maps X to 0, so the guard comes first

    The bottom row, bad, is a case-equality detector watching the bus. It rises in the exact slot where X first appears.

    • Slot 1: d is 1, y follows with 1, and bad is 0.

    • Slot 2: d falls to 0, y follows, and bad stays 0.

    • Slot 3: d is driven to X, y goes X, and bad rises to 1.

    • Slot 4: d is held at X, so y and bad repeat the previous slot.

    • Slot 5: d returns to 1, y follows, and bad falls to 0.

    How X spreads

    Predict first a is X. What are a & 0, a & 1 and a + 1?

    a & 0 is 0: a 0 forces AND to 0 whatever the other bit is. a & 1 is X: the answer depends on the unknown. a + 1 is X in every bit: arithmetic turns any X operand into an all-X result.

    Gates apply controlling-value rules: X & 0 is 0 and X | 1 is 1, but X & 1 and X | 0 stay X. Arithmetic is harsher: if any operand bit is X or Z, the whole result is X, every bit of it.

    Simulation can be pessimistic (reporting X where silicon would settle) or optimistic (reporting a known value where silicon could differ). The dangerous direction is optimistic: an X condition in if silently takes the else branch, and casex matches aggressively despite X.

    Comparisons have X rules of their own: == can return X, === compares the four states literally, and ==? treats X or Z on its right as wildcards. Equality & Wildcards covers all three. The habit for checkers is == plus an explicit $isunknown guard, because === everywhere accepts X-equals-X as a match and hides the bug being hunted.

    Debug X by walking the waveform back to the first X and repeating upstream through its fan-in. The origin is nearly always an unreset flop, an unconnected port, a bus conflict, an out-of-range index, or a don't-care assignment escaping into live logic.

    Testbench

    Full-vector X and Z injection at the boundary between the DUT and testbench code: a case-equality detector, exact-value checks, and a masked compare.

    xmon.sv
    1
    2
    3
    4
    5
    6
    7
    8
    module xmon (
      input logic [3:0] bus,
      output logic bad
    );
      // Full-vector detector: flags an all-X or all-Z bus at the boundary.
      // Partial X (for example 4'b1x00) needs the $isunknown snippet above.
      always_comb bad = (bus === 4'hx) || (bus === 4'hz);
    endmodule
    
    Terminal
    PASS: detector flagged full X and Z, exact checks held, mask compare agreed
    design.sv
    // snippet: wildcard equality says the same thing where the tool honors masks
    if (got ==? 4'b1z00) $display("bit 2 ignored in compare");

    Common pitfall

    Declaring DUT-facing interface signals as bit converts X to 0 before the testbench can see it. Keep pins 4-state and check with $isunknown wherever testbench code samples them.

    Interview

    “Your checker sees X at time 10us, what do you do?” Walk the waveform back to the first X, trace its fan-in cone at that time, and repeat upstream. Name the usual origins: unreset flop, floating port, bus conflict, out-of-range index.

    Check yourself Why does a forgotten assignment to an int variable sail through checks while the same bug on a logic signal waves a flag?

    The int starts at 0, a legal-looking value, while the logic starts at X. The X is a feature: it marks the missing assignment in the waveform.

    Check yourself got holds 4'b1x00, exp holds 4'b1000, and the test uses if (got == exp). Which branch runs, and why is the reason uncomfortable?

    The else branch runs, but only because == returned X and if treats X as false. The test prints the right word for the wrong reason; the lesson pairs a case-equality detector with the $isunknown guard idiom instead.

    Remember

    • 4-state types start at X, 2-state types start at 0; X marks a missing assignment, 0 hides it.

    • Copying 4-state into 2-state turns X and Z into 0 without a warning.

    • Gates can mask X (X & 0 is 0); arithmetic cannot (X + 1 is all X).

    Rule of thumb: keep anything that touches the DUT 4-state, and check $isunknown before a 2-state copy.

    How this was checked

    Run with Icarus 13 (-g2012) on 2026-10-18; the bench printed the PASS line shown, with no warnings. Verilator 5.048 is a 2-state simulator: it turns X and Z into 0 and 1, so it cannot run this lesson's checks.

    The bench injects all-X and all-Z values; the partial-X guard ($isunknown) is shown as a fragment, not run on its own.

    Related topics

    • Next: Nets vs Variables

    Previous

    7 / 363

    Next: Nets vs Variables

    Up next

    Up nextNets vs Variables

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Integer Types & Casting
    • Enums, Structs & Unions
    • X/Z Propagation & Pessimism
    clkdybad
    X injected on a bus, caught by a detector in the same cycle