Verilog full_case and parallel_case: Why They're the "Evil Twins" of Synthesis
Why Verilog's full_case and parallel_case Are Called the "Evil Twins" of Synthesis
Digital Logic Design | Synthesis Directives | Simulation-Synthesis Mismatch — In Depth
The case statement is one of the most frequently used multi-branch constructs in Verilog. It looks straightforward — until you attach the synthesis directives full_case or parallel_case. At that point, an invisible fault line opens between simulation behavior and what the synthesized hardware actually does. Clifford E. Cummings famously labeled these two directives the "Evil Twins of Verilog Synthesis" — a warning the industry has echoed for over two decades. This article unpacks exactly how each directive works, why the mismatch occurs at a structural level, and what modern alternatives eliminate the problem entirely.
How the Verilog case Statement Works by Standard (IEEE 1364)
Before examining the directives, it helps to be precise about the two foundational properties the IEEE 1364 standard guarantees for every case statement.
Priority Evaluation
Items are evaluated top to bottom, in order. If multiple items match simultaneously, only the first one executes. This means a case statement inherently models a priority encoder structure — even when that is not the designer's intent. This matters because synthesis tools may preserve or remove that priority depending on what directives are present.
Incomplete Case Handling
When not all possible input combinations are listed and no default branch exists, the output holds its previous value for any unmatched input. In a combinational logic context, this behavior is exactly the definition of a latch — and an unintentional one at that.
Key Terms Defined
| Term | Definition | Key Property |
|---|---|---|
| Full Case | Every possible input combination (all 2n values) is explicitly listed, or a default branch covers the remainder |
Completeness |
| Parallel Case | No two case items can match the same input simultaneously — items are mutually exclusive | Exclusivity |
Both are legitimate design properties. The danger is not the properties themselves — it is the mechanism used to declare them.
The full_case Directive — What It Does and Why It Backfires
How It Works
Adding // synopsys full_case is a promise to the synthesis tool: "The unlisted cases will never occur at runtime — treat them as don't-cares." The synthesizer takes this at face value and omits the value-holding logic for unmatched inputs, producing a purely combinational result with no latches.
Code Example
case (sel) // synopsys full_case
2'b00: out = a;
2'b01: out = b;
// 2'b10, 2'b11 → synthesizer treats as don't-care
endcase
Synthesis outcome: No latch is inferred even without a default branch. The tool optimizes the missing cases freely, reducing gate count and improving timing.
The catch: If the promise is ever broken — say, a 2'b10 or 2'b11 does appear due to a reset glitch, a CDC (clock-domain crossing) issue, or an unexpected corner case — the hardware produces an arbitrary output determined by the optimizer's don't-care assignment, not by any defined logic.
The parallel_case Directive — Priority Removal and Its Hidden Risk
How It Works
// synopsys parallel_case tells the synthesizer: "The case items are mutually exclusive — skip the priority encoder and build a parallel MUX instead." This can meaningfully reduce area and critical-path delay when the assertion is actually true.
Code Example
case (state) // synopsys parallel_case
3'b1??: out = a;
3'b?1?: out = b;
3'b??1: out = c;
endcase
Synthesis outcome: A parallel MUX structure instead of a priority encoder — smaller and faster.
The gotcha: With the wildcard patterns above, an input of 3'b111 matches all three items simultaneously. The simulator respects priority and selects a. The synthesized hardware, however, activates all three MUX paths concurrently, creating a logical conflict with an unpredictable output. Simulation says pass; silicon says otherwise.
The Root Cause: Simulation-Synthesis Mismatch
This is precisely why these directives earned the "Evil Twins" label. The simulator treats the // synopsys... comment as a comment — it ignores it entirely. The synthesis tool treats the same text as a binding design directive. Two tools, one RTL source, two different interpretations — a structural mismatch by design.
full_case Mismatch Scenario
Simulation: Unspecified inputs (2'b10, 2'b11) cause out to hold its previous value — latch behavior.
Synthesized hardware: The optimizer's don't-care assignment drives out to an arbitrary 0 or 1 — completely uncorrelated with what simulation showed.
parallel_case Mismatch Scenario (More Severe)
Simulation: IEEE 1364 priority semantics apply — only the first matching item executes.
Synthesized hardware: The parallel MUX structure activates multiple data paths simultaneously on overlapping inputs — a bus contention that produces undefined output.
The Core Insight
A design can achieve 100% RTL simulation coverage and still have a gate-level netlist that behaves completely differently. Bugs introduced by full_case/parallel_case do not surface until post-synthesis simulation or physical bring-up on an FPGA or ASIC — making them among the most expensive bugs to find and fix in the entire design cycle.
Simulation vs. Hardware: Side-by-Side Comparison
| Scenario | Simulation Behavior | Synthesized HW Behavior | Risk Level |
|---|---|---|---|
| full_case Unlisted input arrives |
Output holds previous value (latch behavior) | Arbitrary output driven by don't-care optimization | High |
| parallel_case Overlapping input arrives |
First matching item selected (priority) | Multiple paths activate simultaneously — output collision | Critical |
Real-World Impact on Design Quality
Escalating Debug Cost
Bugs that RTL simulation cannot catch only emerge during post-synthesis simulation or board bring-up. The later in the flow a bug surfaces, the more expensive it becomes to fix. For ASIC designs, this can force a respin — a full chip re-fabrication that typically costs millions of dollars and delays tape-out by months. This matters because a single respin can consume the entire project margin.
Unknown State in FSMs
Aggressively using full_case to suppress latches in FSM (finite state machine) next-state logic is particularly risky. If an unlisted state encoding ever appears — due to power-on reset behavior, radiation-induced bit flips, or an undiscovered corner case — the FSM transitions to an unknown state with no defined recovery path, potentially hanging or corrupting the entire system.
Verification Coverage Becomes Meaningless
When simulation no longer faithfully models the synthesized hardware, passing verification metrics no longer guarantee functional correctness. This undermines the entire quality gate of the project — the team believes coverage closes out risk, but the gap exists at a layer coverage cannot see.
Modern Alternatives and Best Practices
Current synthesis flows offer clean, tool-agnostic ways to express the same design intent without relying on comment-based directives that simulators ignore.
Replace full_case: Always write an explicit default branch
Assigning default: out = 'x; achieves two goals simultaneously: the synthesizer can still apply don't-care optimization (because 'x is a legal don't-care hint), while the simulator propagates X values through downstream logic when an unlisted case occurs — making the fault immediately visible in simulation rather than silently wrong in hardware.
case (sel)
2'b00: out = a;
2'b01: out = b;
default: out = 'x; // safe don't-care: X propagates in sim, optimized in synth
endcase
Replace parallel_case: Encode exclusivity in the structure itself
→ If priority matters, express it explicitly with an if-else if-else chain — both the simulator and synthesizer interpret this identically.
→ If items are genuinely non-overlapping, write them as unambiguous literal values without wildcards. The synthesizer will detect mutual exclusivity on its own and optimize accordingly — no directive required, no mismatch possible.
Migrate to SystemVerilog: unique case and priority case
IEEE 1800 (SystemVerilog) promotes these assertions to first-class language keywords — not comments. Both the simulator and synthesizer parse and enforce them as part of the language spec, eliminating any possibility of split interpretation. unique case emits a runtime warning if items overlap or any input goes uncovered, catching violations at the source — during RTL simulation — rather than after tape-out.
| Keyword | Enforces | Simulation Warning On |
|---|---|---|
unique case |
Full + Parallel | Overlap or missing coverage |
priority case |
Full only | Missing coverage |
Practical Audit Checklist
When reviewing existing RTL that uses these directives, work through the following checks before sign-off.
✓ Can a default branch be added to every full_case site? If yes, add it and remove the directive. Prefer default: out = 'x; over default: out = 0; — the former propagates X in simulation; the latter masks the problem.
✓ Do the parallel_case items truly not overlap? Any wildcard (? or z) in a case item is a red flag — enumerate all combinations and verify by inspection or formal tools.
✓ Has gate-level simulation been run? RTL simulation alone cannot expose these mismatches. Post-synthesis netlist simulation with the same testbench is the minimum required check.
✓ Were synthesis warnings reviewed? Most modern tools (Synopsys DC, Cadence Genus, Xilinx Vivado) emit incomplete-case warnings by default. Treat them as errors, not advisory notices.
✓ Is a SystemVerilog migration feasible? For new blocks or significant reworks, adopt unique case/priority case from the start — they enforce the same constraints as the directives, but consistently across all tools.
Let the Language, Not the Comment, Define the Truth
"full_case and parallel_case are tools that allow a designer to lie to the synthesis tool."
— Clifford E. Cummings, SNUG 1999
full_case and parallel_case are artifacts of 1990s ASIC design culture, where shaving even a handful of gates off a block could determine whether a chip hit its area and timing targets. That trade-off — accept simulation-synthesis mismatch in exchange for marginal optimization gains — made some engineering sense when transistors were scarce and synthesis tools had limited optimization headroom. Today, with billions of transistors per die and synthesis engines that routinely infer the same don't-care optimizations from a properly written default: out = 'x;, the gates saved are negligible. The logical gap between simulation and silicon they create is not.
The correct approach in modern RTL design is to express intent through the language itself — not through comments that only half the toolchain reads. A default branch and SystemVerilog's unique case are understood identically by simulator and synthesizer alike. When the RTL source is the single source of truth — not a comment annotation layered on top — you can trust what simulation tells you, and the chip behaves as designed.
References
→ Clifford E. Cummings, "full_case parallel_case, the Evil Twins of Verilog Synthesis" (SNUG 1999)
→ IEEE Standard for Verilog Hardware Description Language (IEEE 1364-2005)
→ IEEE Standard for SystemVerilog (IEEE 1800-2017)
→ Xilinx Vivado Design Suite User Guide: Synthesis (UG901)
This content is provided for informational purposes and does not represent the official position of any tool vendor or standards body. Always consult the documentation for your specific synthesis tool before making implementation decisions.
Curating and personally reviewing resources on semiconductor and SoC design and verification before publishing.
Written based on publicly available data and cited sources. Last updated: June 8, 2026
댓글
댓글 쓰기