Part 1 · Language Foundations · Intermediate
Equality, Case Equality & Wildcards
== vs === vs ==?, casez vs casex dangers with X, case inside, and which to use in RTL versus testbench.
Three equalities, three meanings
SystemVerilog has three flavors of equality because its 4-state values (0, 1, X, Z) force a decision: what should comparing against an unknown mean? Logical equality (==) answers "unknown": if any compared bit is X or Z the result is X, which a if treats as false. Case equality (===) compares the 4-state values literally — X matches X, Z matches Z — and always yields a clean 0 or 1. Wildcard equality (==?) treats X and Z bits in the right-hand operand as don't-cares, making it the operator for masked matching.
EQUALITY OPERATOR SEMANTICS (a vs b)
a b a == b a === b a ==? b
───── ───── ─────── ──────── ─────────
4'b1010 4'b1010 1 1 1
4'b1010 4'b10X0 X (false) 0 1 (X in b = don't care)
4'b10X0 4'b1010 X (false) 0 0 (X in a is NOT a wildcard)
4'b10X0 4'b10X0 X (false) 1 1
== : X anywhere → result X (RTL, synthesizable)
=== : literal 4-state match (TB checks only, not synthesizable)
==? : X/Z in RHS are wildcards (TB masked compares, decode checks)The asymmetry of ==? matters: only the right operand's X/Z bits are wildcards. An X in the left operand still fails the match, which is exactly what you want when checking a possibly-corrupted DUT output against a masked expected pattern.
casez and casex — why casex is banned
Case statements have wildcard variants for decoding. casez treats Z (written ?) as don't-care in both the case expression and the items. casex additionally treats X as don't-care — and that is the danger. In simulation, an uninitialized register or a bus conflict produces X. With casex, that X matches the first item with overlapping bits, so the decoder confidently takes a branch on garbage input and the bug propagates silently instead of failing visibly. Most coding standards ban casex outright and allow casez only with ? literals in the items, never in the selector.
// Priority decoder with casez: ? marks don't-care bits in items
always_comb begin
casez (req[3:0])
4'b1??? : grant = 4'b1000; // req[3] wins regardless of others
4'b01?? : grant = 4'b0100;
4'b001? : grant = 4'b0010;
4'b0001 : grant = 4'b0001;
default : grant = 4'b0000;
endcase
end
// DANGER with casex: if req = 4'bXXXX (uninitialized),
// casex matches the FIRST item 4'b1??? and grants request 3
// on completely unknown input — the X is swallowed, not flagged.
// Safer modern alternative: case inside with ranges & wildcards
always_comb begin
case (req) inside
4'b1??? : grant = 4'b1000;
4'b01?? : grant = 4'b0100;
[4'b0010 : 4'b0011] : grant = 4'b0010; // ranges also allowed
default : grant = 4'b0000;
endcase
endcase inside uses ==? semantics (wildcards only in the items, where you wrote them deliberately) plus set membership with ranges. An X in the selector falls through to default instead of silently matching, which is why it is the recommended replacement for both casez and casex in new RTL.
RTL vs testbench usage rules
Which operator where
RTL: == and case/casez only — === is not synthesizable, and X-optimism differences between sim and synthesis cause mismatches.
Testbench X-checks: === and !== to assert a signal is exactly X or exactly a known value.
Testbench masked compares: ==? with X bits in the expected pattern for don't-care fields.
Decoders in new code: case inside — wildcard items, range support, X in selector hits default.
// Testbench checker patterns
task automatic check_bus(logic [31:0] got, logic [31:0] expect_masked);
// ==? : X bits in expect_masked are don't-care fields
if (!(got ==? expect_masked))
$error("mismatch: got %h expected %h", got, expect_masked);
endtask
initial begin
// === for explicit X detection after reset
if (dut_if.data === 32'hx)
$error("data bus is all-X after reset deassertion");
// mask out byte 0 (don't care) in the expected value
check_bus(rx_data, 32'hDEAD_BExx);
endInterview angle
"Difference between == and ===?" — 4-state X handling; === is TB-only.
"Why is casex dangerous?" — X in the selector matches wildcards, hiding unknowns; casez only treats Z/? as don't-care.
"What replaces casex in modern code?" — case inside, which is X-safe in the selector and supports ranges.
Key takeaways
== returns X when any bit is unknown; if() treats X as false — silent in RTL, so add TB X-checks.
=== compares 4-state literally and is for testbench checks only.
==? makes X/Z in the right operand don't-cares — the masked-compare operator.
Prefer case inside over casez/casex: deliberate wildcards, range support, X-safe selector.
Common pitfalls
Using casex in RTL — an X selector silently matches the first wildcard item and hides real bugs.
Using === in synthesizable code — tools reject it or simulation diverges from gates.
Putting the wildcard pattern on the left of ==? — only RHS X/Z bits are don't-cares.
Relying on == to detect X — the result is X, not 1, so the if-branch never fires.