If you’ve spent enough time working with RTL, you might have encountered the mysterious X in waveforms during simulation. It originates somewhere, then it trickles through your design hierarchy like wildfire. Sometimes, it disappears without explanation.
What is “X“? Well, the “X” can mean different things….
To a synthesizer, an X often represents a don’t-care, which allows the tool to optimize the logic by choosing whichever value (0 or 1) that leads to a simpler implementation. On encountering X, the synthesiser assumes that the designer is not concerned about whether actual hardware will have 0 or 1 value for a specific condition and takes the liberty to optimize the logic.
To a simulator, however, X represents uncertainty or an ambiguity. It indicates that the simulator cannot determine whether the corresponding signal would resolve to a logic 0 or a logic 1 under the given conditions. The X is not a hardware logic state. It is a simulation construct representing an unknown logic state.
Welcome to today’s blog. The X-propagation is our topic for the day. The title of this blog is dedicated to one of my favorite author, Keigo Higashino’s, most famous work.
The usual suspect seen in RTL design simulations who we investigate today is X. Let’s follow the trails and find out if X is really the villain everyone speculate it to be!
The origin of X
We understood what is X. It’s essentially an uncertainty. But, is X really a bad guy? This is an interesting question… Before discussing whether X is a friend or enemy, let’s first explore some of the common scenarios with System Verilog, that generate X during RTL and Gate-level simulation (GLS).
1. Undriven/Uninitialized/Unconnected 4-state signals
// Example 1
logic inv_mode;
assign inv_mode = ~i_mode; // The signal "i_mode" is an input port and is undriven in the design (Z), resulting in "inv_mode" being driven X...
// Example 2
logic out, in;
assign out = ~in; // The signal "in" is an internal signal which is undriven in the design, resulting in "out" being driven X...<br>
2. Uninitialized registers
logic my_ff;
always_ff @(posedge clk) begin
// The designer intends to create mod-2 counter. Reset is not mandatory here. But in simulation, this will be driven X always!
// However, on silicon, it will function correctly as a counter which starts from a logic state of 0 or 1 on power-up!
my_ff <= ~my_ff;
end
3. Multiple drivers conflict/Bus contention
wire out; // Net type = wire, but the data type = logic by default as per SV LRM
// The signal is accidentally driven from two sources with conflicting
// values. According to System Verilog's net resolution rules, the simulator
// resolves the conflict to X.
assign out = 1'b1;
assign out = 1'b0;
4. Designer Intentional
// 3:1 Multiplexer
// A designer may have one or both of the following intentions:
//
// 1) Synthesis optimization:
// The 'sel' is 2 bits wide and can represent four values, but the design
// defines behavior for only 2'b00, 2'b01, and 2'b10. The unused value (2'b11) is
// intentionally assigned X, allowing the synthesizer to treat it as
// a don't-care and potentially optimize the logic.
//
// 2) Verification:
// The design assumes 'sel' can never be 2'b11. If this illegal value
// does occur due to a design bug, assigning X causes the unknown to
// propagate during simulation, making the bug easier to detect.
always_comb begin
case (sel)
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
default: out = 'x;
endcase
end
5. Other reasons
- Power aware simulations (UPF) where X is injected when a power domain is OFF.
- Timing violations at gate-level. Some standard cell models may produce X on GLS (Gate-Level Simulation).
I would like to summarize that-
An X is neither friend nor foe. Whether it’s good or bad, depends on why it’s there and how it’s used… It could be an evidence or hint that points to a real design bug. Or sometimes it’s just a modeling artifact at gate-level which can be ignored. Or sometimes it is intentionally placed by the designer for optimization. The challenge is how to distinguish between these cases...
X-propagation and its impact on Verification
X-propagation is the process by which an unknown logic value (X) becomes a driver and influences the values of other signals as it travels through an RTL design hierarchy during simulation. Since X represents uncertainty, any logic that depends on an X may also (or may not) become uncertain.
The goal of X-propagation is not to detect Xs or generate more Xs, but to prevent RTL simulation from making arbitrary decisions that can hide real hardware bugs. The goal of X-propagation is to faithfully represent uncertainty, so that RTL simulation behaves as closely as possible to the intended hardware behavior on Silicon. If it fails to propagate X correctly, it will lead to mismatch in Gate-level simulation and RTL simulation. This makes verification painful…
There are two ways a simulator handles X-propagation: X-optimism and X-pessimism.
X-optimism and X-pessimism
The simulation semantics of certain RTL constructs (System Verilog, in the context of this blog) do not always faithfully represent hardware uncertainty.
X-optimism occurs when the simulator transforms unknown logic value (X) in an expression in the RTL to produce a deterministic output instead of propagating the uncertainty to the output. Consequently, a real functional bug may remain hidden during RTL simulation, allowing it to escape the verification process and potentially surface only later in GLS or, worse, in silicon. This leads to costly design iterations, Silicon re-spin etc…
X-pessimism occurs when the simulator propagates an unknown logic value (X) even though the actual hardware would always produce a deterministic 0 or 1. In other words, the simulator pessimistically estimates the uncertainty, resulting in unwanted X propagation. X-pessimism is more commonly observed in GLS, where standard-cell library models are modeled intentionally conservative to avoid masking potential hardware uncertainties. X-pessimism obscures the real behavior of the design, making waveform debugging more difficult and potentially leading engineers to investigate false issues that would never occur on silicon.
A classic example of X-pessimism is a mod-2 counter implemented without reset-
logic my_ff;
always_ff @(posedge clk) begin
my_ff <= ~my_ff;
end
Here, the counter will be stuck at X on simulation. But on actual silicon, the flip-flop might power up as either a 0 or a 1 and it will function correctly as mod-2 counter!
Other classic examples include System Verilog’s equality, relational and arithmetic operators, which are X-pessimistic. Any uncertainty (bit with X or Z) in an operand will propagate X to the output of the expression. This sometimes propagates an X when no hardware uncertainty actually exists on Silicon.
// Example 1
logic [2:0] a, b, sum;
assign a = 3'b000;
assign b = 3'b10x;
assign sum = a + b; // Here, sum='x in simulation. While on Hardware, it would be 3'b10x, uncertain only on the LSB, which is correct.
// Example 2
logic [2:0] a, b, prod;
assign a = 3'b11X;
assign b = 3'b000;
assign prod = a * b; // Here, prod='x in simulation, but anything multiplied with 0 is 0. On Hardware, it would be 0 as expected, no uncertainty!
// Example 2
logic [2:0] a, b;
logic op;
assign a = 3'b100;
assign b = 3'b00x;
assign op = (a>b); // Here op=X in simulation, but a is obviously greater than b regardless of LSB of b! Hardware will output op=1, without any uncertainty...
Thus, X-pessimism creates “false alarms” while there is no uncertainty at all on actual silicon, leading to unnecessary and wasteful debug efforts.
From the definitions, you may have already realized that X-optimism is generally more dangerous than X-pessimism.
- While X-pessimism may create false failures and increase debugging effort, X-optimism can silently mask real design bugs!
- X-optimism may also lead to false code and functional coverage. Since the simulator may execute code paths that would not be deterministically taken in hardware. As a result, coverage metrics may indicate that certain branches, statements, or functional scenarios have been exercised, giving a misleading impression of verification completeness….
This is all too much theory eh!? 🤓
Let’s get into some real examples in RTL to understand X-optimism and how it masks bug in the design during the simulation…
X-propagation in a simple Multiplexer
Let’s analyze five different RTL implementations of a simple 2:1 multiplexer.
We are interested in the case where the select goes X in the simulation due to some potential bug in the design. All signals are assumed to be logic 4-state variables/wires.
1. if-else construct
// If-else
//
// sel a b out(RTL) out(Silicon)
// x 0 0 0 0
// x 0 1 1 0 or 1
// x 1 0 0 0 or 1
// x 1 1 1 1
//
// Verdict:
// - Output is same as b when sel = X
// - RTL sim masks the uncertainty that could happen on actual Silicon
// - X-optimistic
always_comb begin
if (sel) out = a;
else out = b;
end
As per System Verilog semantics, in an if statement, the condition evaluates to True, only when atleast one bit in the condition is 1.
So, in this example, when sel = X, the else branch is taken, and out is assigned = b. This behavior concurs with the actual Silicon behavior as long as the inputs are same, a = b when sel = X. But when the inputs are different, a != b when sel = X, there is uncertainty on actual Silicon. While the RTL simulator drives out with whatever value is in b!
This is a classic example of X-optimism, where the RTL simulator makes a deterministic decision instead of faithfully representing the uncertainty that would exist on Silicon! Since the uncertainty (X) is never propagated, the potential functional bug may remain hidden throughout RTL verification.
2. Conditional / Ternary operator
// Ternary operator
// ---------------
// sel a b out(RTL) out(Silicon)
// x 0 0 0 0
// x 0 1 x 0 or 1
// x 1 0 x 0 or 1
// x 1 1 1 1
//
// Verdict:
// - RTL sim faithfully represents the uncertainty that could happen on actual Silicon
// - No X-optimism/pessimism
assign out = sel? a : b;
As per System Verilog semantics, in a conditional operator, the condition evaluates to True, only when atleast one bit in the condition is 1. If all bits are 0, then the condition evaluates to False. In all other cases, the condition evaluates to neither True nor False; the condition is instead evaluated as “unknown“. In that case, the operator will perform a bit-by-bit comparison of the values of the two expressions in the RHS. For each bit position, if the bit is 0 in both expressions, then 0 is returned for that bit. If both bits are 1, then 1 is returned. If the corresponding bits in each expression are different, or Z, or X, then an X is returned for that bit.
In this case when sel = 1’bX, the inputs a and b have different values. Therefore, out is assigned = X.
Unlike the previous example, the RTL simulator preserves the uncertainty instead of making a deterministic decision. This faithfully represents the actual Silicon behavior. As a result, the uncertainty (X) is allowed to propagate, increasing the chances that the issue will be exposed during RTL verification.
3. Case statement (with default and no default)
// Case without default
// --------------------
// sel a b out(RTL) out(Silicon)
// x 0 0 old val 0
// x 0 1 old val 0 or 1
// x 1 0 old val 0 or 1
// x 1 1 old val 1
//
// Verdict:
// - X matches no case item when sel = X
// - No assignment occurs and previous value may get retained
// - RTL sim masks the uncertainty that could happen on actual Silicon
// - X-optimistic/pessimistic
always_comb begin
case (sel)
1'b0: out = b;
1'b1: out = a;
endcase
end
// Case with default
// -----------------
// sel a b out(RTL) out(Silicon)
// x 0 0 0 0
// x 0 1 0 0 or 1
// x 1 0 1 0 or 1
// x 1 1 1 1
//
// Verdict:
// - X matches default item
// - Output is same as b when sel = X
// - RTL sim masks the uncertainty that could happen on actual Silicon
// - X-optimistic
always_comb begin
case (sel)
1'b1 : out = a;
default: out = b;
endcase
end
The first example is a Case without default. This is interesting because the case statement is apparently full case with all possible case items, so the designer choose to add no default statement. But the way System Verilog semantics work, when sel = X, the X doesn’t match with any of the case items, and hence out will retain its previous value in RTL simulation! This behavior can be X-optimistic or X-pessimistic, because the previous value of out can be 0/1/X/Z.
The second example is a full Case with default. This behaves X-optimistic in RTL simulation when sel = X, similar to the if statement we discussed earlier.
4. Using Gates
// Gates
// -----
// sel a b out(RTL) out(Silicon)
// x 0 0 0 0
// x 0 1 x 0 or 1
// x 1 0 x 0 or 1
// x 1 1 x 1
//
// Verdict:
// - Output is X when sel = X
// - RTL sim represents uncertainty even when there is no uncertainty on actual Silicon (when sel=X, a=b=1)
// - X-pessimistic
assign out = (~sel & b) | (sel & a); // sel=X means ~sel=X too, so out = X) | X = X
In this example, the 2:1 mux is implemented using AND, NOT, and OR gates. When sel = X, out is assigned = X because the gate operators cannot resolve the output. However, on actual Silicon, if both inputs a and b have the same value, the output is deterministic and out will always be driven to that value, regardless of the unknown state of sel = X.
Interestingly, this behavior in RTL simulation closely resembles that of GLS. This is because the gate operators in SV use four-state logic that is inherently X-pessimistic, much like the standard cell models used in GLS. As a result, the RTL simulator may propagate X even when the actual hardware output is deterministic, making it X-pessimistic.
Now that we have discussed X-propagation in a 2:1 mux with different coding styles in RTL, I want to assert that-
These behavioral differences exist only during simulation. From the synthesis tool’s perspective, all above coding styles describe the same 2:1 multiplexer. The synthesis tool does not preserve RTL simulation semantics for unknown (X) values; it treats them as don’t-cares during optimization. As a result, X-optimism or X-pessimism is purely a simulation phenomenon.
Well, X may be baffling you, if you are an RTL designer. Should you explictly take care of X inputs in your RTL!?
How to handle X in the RTL Code?
As an RTL designer, should you worry about X at all? Should you describe RTL to eliminate X-optimism and make the simulation “X-accurate”?
Here are a few approaches you might be tempted to take in an attempt to be “X-accurate”:
- Replace if-else with ternary operator – This may work for simple expressions like a multiplexer we saw before. But this can quickly become error prone, unreadable, and difficult to construct and debug for complex expressions. Besides, if the condition is multi-bit, the ternary operator also becomes X-optimistic. When one of the bit in the condition is 1, the condition deterministically evaluates to False, while there may be some other bits which are X… Advantage lost!
- Implement the logic using gate-level operators – Another approach is to describe a functionality in RTL using explicit gate operations instead of behavioral constructs. While this often produces behavior closer to GLS, it merely shifts the problem from X-optimism to X-pessimism. More importantly, it sacrifices the abstraction and readability of RTL, making the code harder to understand, verify, and maintain. This beats the whole purpose of RTL’s existence –> The abstraction of behavior modelling!
- Assigning X to the signals in the default of case statement or using explicit else-if for X – This may help to expose illegal or unexpected states by propagating uncertainty in some cases. It’s not a universal solution and may instead introduce X-pessimism. Such false alarms can lead to unnecessary and wasteful debug effort during RTL verification.
- Explicitly handle every X combination in the RTL – One approach is to enumerate the possible aunknown cases and manually assign the output that best matches the expected hardware behavior. While this can make a specific block/functionality more X-accurate, it is tedious, error-prone, and difficult to scale. Every conditional expression must be carefully analyzed, and every X combination must be considered. As the complexity of the design grows, the number of cases increases rapidly, making the RTL harder to write, review, and maintain.
All of the above approaches have its caveats and are not practicable in all cases.
What we need to acknowledge is: X-optimism is not the result of poor RTL coding; it is a consequence of the simulation semantics defined by the HDL (SV, VHDL etc..). While explicitly enforced coding techniques can reduce X-optimism in isolated cases, they place the burden entirely on the RTL designer to anticipate every possible source of uncertainty. Such an approach is neither practical nor scalable!
What we really need is a systemic solution, that understands hardware semantics. This is precisely the motivation behind X-propagation (“X-prop”) feature in modern EDA tools. Instead of expecting designers to manually rewrite RTL to account for every X, the X-prop enhances the simulator itself, making RTL simulation more faithfully represent the uncertainty that can exist in real hardware.
X-prop in EDA tools
The objective of X-prop is to make RTL simulation faithfully represent the uncertainty that may exist in real hardware. Conventional RTL simulation follows the System Verilog language semantics, which can either hide uncertainty (X-optimism) or introduce false uncertainty (X-pessimism).
X-prop addresses both of the above issues by propagating X only when the hardware outcome is genuinely uncertain on Silicon. Thus, X-prop helps to faithfully represent the uncertainty.
I don’t want to dig deep into how these tools work its way through in the simulation for each construct in SV. But conceptually-
When an X influences a decision in an expression, the simulator evaluates all possible hardware interpretations of that unknown value. For example, if a 1-bit control signal is X, the simulator evaluates the expression assuming two possible cases: signal is 0, signal is 1.
The results of the expression in both cases are then merged:
- If both evaluations produce the same output, that value is assigned because the hardware behavior on Silicon is deterministic.
- If the evaluations produce different outputs, the result is assigned X, indicating that the hardware behavior on Silicon is uncertain.
We have seen earlier how X-optimism masks the X at output of an if-else based 2:1 mux when sel = X. The below figure shows how it’s handled by X-prop when sel = X, reproducing the uncertainty correctly.
X-prop merge on a if-else based 2:1 mux
X-prop makes life easier for RTL design/verification engineers-
- Reduces X-optimism. Exposes functional bugs in RTL that maybe hidden in conventional RTL simulation to due to X-optimism. These bugs are caught early at the RTL simulation stage. It is substantially cheaper than diagnosing them during GLS.
- Minimizes RTL simulation to GLS mismatches by accurately modelling uncertainty that could happen on Silicon.
- Designers no longer need to rely on defensive X-aware coding styles in RTL (like the approaches we discussed earlier!)
- Reduces X-pessimism. This avoids false alarms and unnecessary debug effort on reported uncertainties that never really exist!
- Improves coverage accuracy. We already discussed earlier how X-optimism leads to misleading functional and code coverage. By reducing X-optimism, coverage metrics more accurately reflect the scenarios that have been genuinely verified, increasing confidence in verification closure.
Support
Leave a comment or visit support for any queries/feedback regarding the content of this blog.
If you liked Chipmunk , don’t forget to follow!:

Related