Skip to content

SystemVerilog · Module 5

if-else & Unique/Priority Modifiers

Hardware inferred, simulation checks, coverage and synthesis safety.

Module 5 · Page 5.2

Basic if-else — The Foundation

if-else inside always_comb infers a priority mux chain — the first condition that is true wins. Every subsequent condition is checked only if all previous ones were false. This is the correct mental model for both simulation and synthesis.

SystemVerilog — if-else: all forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Form 1: simple if ─────────────────────────────────────────────
always_comb begin
    out = data;                // default
    if (enable) out = data_in; // override when enable=1
end
 
 
// ── Form 2: if-else ───────────────────────────────────────────────
always_comb begin
    if (sel)
        y = a;
    else
        y = b;      // 2-to-1 mux — every path assigns y
end
 
 
// ── Form 3: if-else if-else chain (priority mux) ─────────────────
always_comb begin
    out = '0;               // default: prevents latch
    if      (req[0]) out = data_a;  // highest priority
    else if (req[1]) out = data_b;
    else if (req[2]) out = data_c;
    else             out = data_d;  // lowest priority
end
// If req[0] and req[2] are both 1, req[0] wins — implicit priority
 
 
// ── Form 4: nested if ─────────────────────────────────────────────
always_comb begin
    result = '0;
    if (valid) begin
        if (write)  result = write_data;
        else        result = read_data;
    end
end

Figure 1 — if-else if-else Infers a Priority Mux Chain

Tutorial diagram

Figure 1 — A 3-level if-else if chain infers 3 chained 2:1 muxes. The select line of each mux is one condition. req[0] has the highest priority — if it's true, none of the later conditions matter.

🏗 Synthesis Concern: if-else Chain = Critical Path Problem

Every level of if-else if adds one more mux in series. A 4-level chain produces 4 cascaded 2:1 muxes — the signal must propagate through all of them before settling. In a 500MHz design with a 2ns clock budget, each mux adds ~150–250ps. A 4-level chain can burn 600–1000ps of your timing budget just in the mux tree. When conditions are mutually exclusive, unique if collapses this to a single-level parallel mux — recovering that timing immediately.

🧠 How the Simulator Evaluates if-else in always_comb

When any input to an always_comb block changes, the entire block re-evaluates in the Active region. The simulator processes the if-else if chain top to bottom — evaluating each condition expression left-to-right — and takes the first branch whose expression is true. Subsequent conditions are short-circuited: they are never evaluated. This exactly mirrors how a hardware priority mux works, where the first-asserted select overrides all others. The simulation model is bit-accurate with the synthesized hardware — as long as conditions do not overlap.

The Problem Plain if Cannot Solve

Plain if has two limitations that matter in real hardware design:

Overlapping Conditions

If two conditions can both be true simultaneously, the first one silently wins. There is no simulation warning. This can mask a design bug — you think conditions are mutually exclusive, but they are not.

Missing Condition Coverage

If no condition in a chain is ever true, and you forgot the final else, the output is unchanged — a latch is inferred. Again, no warning. The tool creates hardware you did not intend.

Synthesis Adds Priority Gates

Even if your conditions are truly mutually exclusive, synthesis still generates a priority chain (extra logic). You lose area and timing performance for no reason.

SystemVerilog addresses all three with decision modifiers: unique if, unique0 if, and priority if. The same modifiers apply to case — see case, casex and casez for the statement form and its own set of traps.

unique if — Mutually Exclusive Conditions

unique if makes two guarantees explicit: (1) at most one condition can be true at any given time (mutually exclusive), and (2) the simulator checks this during simulation and issues a warning if two conditions are simultaneously true. The synthesis tool uses the guarantee to optimise — it can generate a parallel mux instead of a priority chain, reducing logic depth. Figure 2 — unique if Enables Parallel Mux (No Priority Chain)

Tutorial diagram

Figure 2 — unique if tells the tool conditions are mutually exclusive: synthesis generates a parallel mux (1 logic level) instead of a priority chain (N levels). Simulation warns if two conditions are simultaneously true.

SystemVerilog — unique if: syntax, behaviour, simulation checks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── unique if: conditions are mutually exclusive ──────────────────
always_comb begin
    out = '0;
    unique if (mode == 2'b00)  out = a + b;
    else if  (mode == 2'b01)  out = a - b;
    else if  (mode == 2'b10)  out = a & b;
    else                       out = a | b;
end
// Tool knows: only one condition is ever true at a time
// → Generates parallel mux, not priority chain
// → Simulator: warns if mode matches more than one branch (impossible here, but checked)
 
 
// ── unique if: simulation warning demonstration ───────────────────
always_comb begin
    out = '0;
    unique if (a && !b)  out = 8'hAA;
    else if  (!a && b)  out = 8'hBB;
    else if  (!a && !b) out = 8'h00;
    // What if a=1 and b=1? No branch matches!
    // → Simulator issues a WARNING: "unique if: no condition is true"
    // This is a design bug that plain if would SILENTLY ignore
end
 
 
// ── unique inside always_ff: use unique0, or plain if ─────────────
// ✗ WRONG. Whenever rst_n is high and en is low - the normal idle
//   condition of most flops - NO branch is taken, and unique if
//   reports a no-match violation. Every cycle. In every test.
//     unique if (!rst_n) q <= '0;
//     else if  (en)      q <= d;
//
// ✓ unique0 checks exclusivity but does NOT require a branch to be
//   taken, which is exactly the semantics an incomplete chain wants.
always_ff @(posedge clk) begin
    unique0 if (!rst_n)  q <= '0;
    else if   (en)       q <= d;
end
// ✓ plain if is also perfectly correct here: the priority is
//   intentional and there is nothing to check.

unique if: the overlap violation and the no-match violation

6 cycles
Three windows showing a normal match, an overlap violation, and a no-match violation, with the corresponding simulator reportsone condition true — branch A taken, nothing reportedone condition true — branchA taken, nothing reportedboth true — overlap violation reported; branch A still winsboth true — overlapviolation reported; branchA still winsneither true and no final else — no-match violation; out holds its defaultneither true and no finalelse — no-match violation;out holds its defaultclkcond_acond_boutAAAAdfltdfltviolationt0t1t2t3t4t5
Figure 2b — what the two unique if violation classes look like on a timeline. In the normal window one condition matches, the branch executes, and nothing is reported. In the overlap window two conditions are simultaneously true, so unique if reports a multiple-conditions violation while the first branch still executes — the hardware behaviour is unchanged, only the report is new. In the no-match window no condition is true and there is no final else, so unique if reports a no-condition violation and the target keeps its default. With plain if both windows are completely silent, which is the point: the modifier does not change what the hardware does, it changes whether you find out.

With plain if, both violation windows above are silent and the bugs reach silicon undetected. With unique if they surface in regression. Note that in the overlap window the hardware is identical either way — the first matching branch wins in both cases. The modifier buys visibility, not different behaviour.

⚠ When NOT to Use unique if — The X-State Trap

If any signal driving a unique if condition is X — which is the normal state at time zero before reset, and during X-propagation — no comparison evaluates to true, so the chain reports a no-match violation on every evaluation. The design is functionally correct; the reports describe the reset window.

Note what does not work here. These are violation reports on a procedural statement, not concurrent assertions, so there is no disable iff to attach and $assertoff is not defined by IEEE 1800 to control them — some tools extend their assertion controls to cover them and some do not, so anything built on that is tool-specific and quietly non-portable. The portable fixes are structural:

  1. Give the chain a final else. No-match becomes unreachable, the uniqueness check is untouched, and the X case lands in a defined state instead of holding.
  2. Use unique0 if. It checks that at most one condition is true and drops the requirement that any branch be taken — the right modifier whenever the conditions are genuinely exclusive but deliberately incomplete. This is usually the cleanest answer.
  3. Use priority if if the conditions can overlap anyway, which keeps the no-match check while dropping the overlap check.

priority if — Explicit Priority, No Overlap Warning

priority if declares that conditions may overlap, and the first-matching branch intentionally wins. The simulator checks that at least one condition is always true (warns if none match), but does NOT warn if multiple conditions are true — overlap is expected. The synthesis tool generates an optimised priority encoder rather than a plain mux chain.

SystemVerilog — priority if: overlapping conditions are intentional
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── priority if: overlapping conditions OK — first wins ───────────
always_comb begin
    irq_id = 3'b000;
    priority if (irq[0]) irq_id = 3'd0;  // IRQ0 has highest priority
    else if     (irq[1]) irq_id = 3'd1;
    else if     (irq[2]) irq_id = 3'd2;
    else if     (irq[3]) irq_id = 3'd3;
    else if     (irq[4]) irq_id = 3'd4;
end
// irq[0] and irq[3] both asserted → irq_id = 0 (intentional, no warning)
// No interrupts asserted         → simulator WARNS: "no priority if branch taken"
 
 
// ── priority if vs plain if — what changes? ───────────────────────
// Plain if:    • No simulation checks. Tool generates priority chain anyway.
// priority if: • Simulator warns if no branch matches (helps catch unhandled states)
//              • Tool KNOWS priority is intentional → may optimise encode logic
// unique if:   • Simulator warns if overlap OR no-match (strictest checking)
//              • Tool generates parallel mux (no priority chain needed)
// unique0 if:  • Warns on overlap, but NOT on no-match
//              • For conditions that are exclusive but deliberately
//                incomplete — the usual fix for reset-window X warnings

🚀 RTL Design Insight: priority if Is the Correct Tool for Interrupt Controllers

Every real SoC has an interrupt controller. Multiple interrupt sources can assert simultaneously — that is not a bug, it is the normal operating mode. Using priority if explicitly declares this to the tool: "multiple conditions can be true simultaneously, and I want the highest-priority (first-listed) branch to win." The synthesis tool generates an optimised priority encoder — not a simple mux chain — which is both area-efficient and timing-clean. Using plain if achieves the same hardware but loses the simulation safety net (no warning when no IRQ is asserted). Using unique if is actively wrong here — it reports a violation on every clock cycle where multiple IRQs are pending, which is the controller's normal operating mode rather than a fault. The same reasoning drives arbiter design generally; see AXI arbitration for how fixed priority compares with round-robin when starvation matters.

Choosing: plain, unique, or priority?

Figure 3 — Which if Modifier Should You Use?

Tutorial diagram

Figure 3 — Decision tree for choosing between plain if, unique if, and priority if. The choice affects both simulation checking and the hardware the tool generates.

KeywordConditions can overlap?Sim warns on overlap?Sim warns on no-match?Hardware inferred
ifYes (silent)NoNoPriority mux chain
unique ifNo — must be exclusiveYes ✅Yes ✅Parallel mux (optimised)
unique0 ifNo — must be exclusiveYes ✅NoParallel mux (optimised)
priority ifYes — intentionalNoYes ✅Optimised priority encoder

Real-World Examples

SystemVerilog — Three Real Patterns: plain, unique, priority
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ Example 1: plain if — for simple sequential decisions ═══════
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else if (start)
        state <= BUSY;
    else if (done)
        state <= DONE;
    else
        state <= state;
end
// Plain if is fine here: rst_n, start, done are designed to not overlap
// See /learnings/systemverilog/procedural-blocks for always_ff vs always_comb
 
 
// ════ Example 2: unique if — ALU opcode decode (one-hot) ══════════
always_comb begin
    alu_out = '0;
    unique if (op == 4'h0) alu_out = a + b;     // ADD
    else if  (op == 4'h1) alu_out = a - b;     // SUB
    else if  (op == 4'h2) alu_out = a & b;     // AND
    else if  (op == 4'h3) alu_out = a | b;     // OR
    else if  (op == 4'h4) alu_out = a ^ b;     // XOR
    else if  (op == 4'h5) alu_out = ~a;       // NOT
    else if  (op == 4'h6) alu_out = a << 1;   // SHL
    else if  (op == 4'h7) alu_out = a >> 1;   // SHR
end
// op is a 4-bit field — only one opcode is ever encoded at a time
// unique if: warns if somehow two ops match, warns if op=8..15 (unhandled)
// synthesis: generates parallel logic, not a chain of 8 muxes
 
 
// ════ Example 3: priority if — interrupt controller ════════════════
always_comb begin
    irq_vec = 8'h00;
    irq_num = 3'd0;
    priority if (irq[7]) begin irq_vec[7] = 1; irq_num = 7; end
    else if     (irq[6]) begin irq_vec[6] = 1; irq_num = 6; end
    else if     (irq[5]) begin irq_vec[5] = 1; irq_num = 5; end
    else if     (irq[4]) begin irq_vec[4] = 1; irq_num = 4; end
    else if     (irq[3]) begin irq_vec[3] = 1; irq_num = 3; end
    else if     (irq[2]) begin irq_vec[2] = 1; irq_num = 2; end
    else if     (irq[1]) begin irq_vec[1] = 1; irq_num = 1; end
    else if     (irq[0]) begin irq_vec[0] = 1; irq_num = 0; end
end
// Multiple IRQs pending simultaneously is EXPECTED — priority if is correct
// Simulator: no overlap warning (intentional), warns if irq == 0 (nothing pending)

if inside always_ff — Reset and Enable Patterns

Inside always_ff, the if statement controls which register action fires — reset, enable, or hold. The pattern is standardised across the industry. Mastering it is essential for all RTL design.

SystemVerilog — if patterns inside always_ff (register templates)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Template 1: async reset only ─────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) q <= '0;
    else        q <= d;
end
 
 
// ── Template 2: async reset + synchronous enable ──────────────────
always_ff @(posedge clk or negedge rst_n) begin
    if      (!rst_n) q <= '0;    // async reset — highest priority
    else if (en)     q <= d;     // sync load when enabled
    // else: q holds its value (no assignment = hold)
end
 
 
// ── Template 3: async reset + sync load + sync clear ─────────────
always_ff @(posedge clk or negedge rst_n) begin
    if      (!rst_n) q <= '0;    // async reset
    else if (clr)    q <= '0;    // sync clear (higher priority than load)
    else if (load)   q <= d;     // sync load
    // else: hold
end
 
 
// ── Template 4: shift register with if ───────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        shreg <= 8'h00;
    else
        shreg <= {shreg[6:0], serial_in};  // shift left, new bit enters from right
end

🔍 Debugging Insight: Priority of if Branches inside always_ff Is Absolute

In an always_ff block, the first if branch always wins — this is not a style choice, it is the hardware reality. The classic template is: reset first (async), then synchronous clear, then synchronous load, then hold. If you accidentally write else if (!rst_n) instead of if (!rst_n) as the first branch, the reset becomes gated by the previous condition — a functional bug that only appears during specific timing scenarios where reset is asserted while the earlier condition is also true. Always verify reset is the first, unconditional if branch inside always_ff with async reset.

Common Mistakes

SystemVerilog — if-else mistakes and fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ MISTAKE 1: Missing default in always_comb — infers latch ═══
always_comb begin
    if (sel == 2'b00) out = a;   // ❌ what about sel = 2'b01, 10, 11?
    else if (sel == 2'b01) out = b;  // tool errors: latch inferred for 'out'
end
// ✅ FIX: add default or final else
always_comb begin
    out = '0;                        // default
    if      (sel == 2'b00) out = a;
    else if (sel == 2'b01) out = b;
end
 
 
// ════ MISTAKE 2: Confusing unique if and priority if ══════════════
// IRQ arbiter — conditions CAN overlap (multiple IRQs at once)
always_comb begin
    unique if (irq[0]) id = 0;   // ❌ unique if for IRQs that can overlap!
    else if   (irq[1]) id = 1;   // Simulator will warn every clock cycle
end
// ✅ FIX: use priority if when overlap is intentional
always_comb begin
    priority if (irq[0]) id = 0;  // overlap OK — no warning
    else if     (irq[1]) id = 1;
end
 
 
// ════ MISTAKE 3: Using = instead of <= in always_ff ══════════════
always_ff @(posedge clk) begin
    if (en) q = d;   // ❌ blocking in always_ff → covered in 5.6
end
// ✅ FIX: always use non-blocking inside always_ff
always_ff @(posedge clk) begin
    if (en) q <= d;  // ✅ correct
end

Quick Reference — if-else Cheat Sheet

SystemVerilog — if-else Quick Reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Combinational if (always_comb) ────────────────────────────
always_comb begin
    out = '0;               // ALWAYS add default first!
    if      (cond_a) out = a;
    else if (cond_b) out = b;
    else             out = c;
end
 
// ── unique if: mutually exclusive, both overlap and no-match warned
always_comb begin
    out = '0;
    unique if  (op==2'b00) out = a;  // one-hot / enum decode
    else if (op==2'b01) out = b;
    else if (op==2'b10) out = c;
    else                out = d;
end
 
// ── priority if: overlap OK, warns on no-match ─────────────────
always_comb begin
    id = '0;
    priority if (req[0]) id = 0;   // interrupt / priority encoder
    else if     (req[1]) id = 1;
    else if     (req[2]) id = 2;
end
 
// ── Sequential if (always_ff) ─────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
    if      (!rst_n) q <= '0;   // async reset
    else if (en)     q <= d;    // sync load
    // else: hold (no assignment = correct register hold)
end
 
// ── Decision guide ────────────────────────────────────────────
// Conditions mutually exclusive + must match?  → unique if
// Conditions can overlap, priority needed?     → priority if
// No special checking needed?                  → plain if
// Always add default assignment in always_comb!

🧠 Waveform & Simulation — How if-else Actually Evaluates

Understanding what actually happens inside the simulator when an if-else if chain evaluates is the foundation of debugging priority-logic bugs. The simulator is deterministic — same inputs always give same output — but the priority chain means inputs evaluated earlier in the chain completely suppress later branches.

Priority chain: the first-listed asserted request always wins

8 cycles
Four overlapping request signals resolved by a fixed priority chain, showing req0 winning whenever asserted and lower requests waitingreq0 and req1 both asserted — req0 wins, req1 waitsreq0 and req1 both asserted— req0 wins, req1 waitsreq0 and req2 both asserted — req0 wins againreq0 and req2 both asserted— req0 wins againreq0 clear; req1 now beats req2req0 clear; req1 now beatsreq2req2 finally wins once both higher requesters are clearreq2 finally wins once bothhigher requesters are clearclkreq[0]req[1]req[2]req[3]outdfltd_ad_ad_ad_bd_bd_cd_dt0t1t2t3t4t5t6t7
Figure 4 — a priority chain resolving overlapping requests. Four requesters assert in overlapping patterns. Because req[0] is listed first it wins in every cycle where it is asserted, regardless of what else is pending; req[1] wins only once req[0] has dropped; req[2] only once both higher requesters are clear. The output therefore follows the highest-numbered priority currently asserted, and lower requesters simply wait. This is the intended behaviour of priority if, and it is also the starvation risk: a requester that is never the highest priority never wins, which is why fixed-priority arbitration is unsuitable when every requester must eventually be served.

At T2 and T3: multiple req bits are high simultaneously. With plain if, this is completely silent — the first true condition wins, all others are ignored. With unique if, the simulator fires a warning at T2 and T3 alerting you to the overlap. If your spec says only one request can be active at a time, those warnings identify a real upstream bug.

SystemVerilog — Simulation-Level Verification of if Modifier Behavior
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Testbench: proves unique if catches overlap, priority if doesn't ─
module tb_modifier_check;
    logic [3:0] req;
    logic [7:0] out_unique, out_priority;
 
    // DUT A: unique if — should warn when req[0] && req[1] both high
    always_comb begin
        out_unique = 8'h00;
        unique if (req[0]) out_unique = 8'hAA;
        else if  (req[1]) out_unique = 8'hBB;
        else if  (req[2]) out_unique = 8'hCC;
        else if  (req[3]) out_unique = 8'hDD;
    end
 
    // DUT B: priority if — no warning on overlap, intentional
    always_comb begin
        out_priority = 8'h00;
        priority if (req[0]) out_priority = 8'hAA;
        else if     (req[1]) out_priority = 8'hBB;
        else if     (req[2]) out_priority = 8'hCC;
        else if     (req[3]) out_priority = 8'hDD;
    end
 
    initial begin
        req = 4'b0000; #10;   // no requests — unique if warns (no match)
        req = 4'b0001; #10;   // req[0] only — clean, no warning
        req = 4'b0011; #10;   // req[0] AND req[1] — unique if WARNS, priority if silent
        req = 4'b0110; #10;   // req[1] AND req[2] — unique if WARNS, priority gives BB
        req = 4'b1000; #10;   // req[3] only — clean
        $display("out_unique=%h out_priority=%h", out_unique, out_priority);
        $finish;
    end
endmodule
 
// ── Expected Simulation Output ─────────────────────────────────────
// WARNING: unique if violation — all conditions are false (at req=0000)
// WARNING: unique if violation — multiple conditions are true (at req=0011)
// WARNING: unique if violation — multiple conditions are true (at req=0110)
// out_unique=dd out_priority=dd  (final state: req=1000, no overlap)

🏗 Synthesis Deep Dive — What Hardware Each Form Actually Generates

The choice between plain if, unique if, and priority if is not just a simulation checking decision — it directly impacts the logic depth, area, and timing of your synthesized netlist.

FormSynthesized StructureLogic LevelsTiming ImpactArea Impact
if-else if (plain, N conditions)N cascaded 2:1 muxes in series (priority chain)N levelsWorst — critical path grows linearly with NN × mux cells
unique if (N mutually exclusive conditions)Parallel N:1 mux or one-hot decoder + mux1–2 levelsBest — all inputs evaluated simultaneouslySimilar or less than priority chain
priority if (N overlapping conditions)Priority encoder + mux (optimised, not naive chain)log₂(N) levelsBetter than plain if — encoder is optimisedPriority encoder + single mux
SystemVerilog — Same Logic, Three Forms, Different Netlists
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Scenario: 4-input mux, one-hot select ─────────────────────────
// The spec says only one of {s0,s1,s2,s3} is ever high at a time.
 
// Form A: plain if — synthesis generates 3 chained muxes
always_comb begin
    out = '0;
    if      (s0) out = a;   // mux1: sel=s0
    else if (s1) out = b;   // mux2: sel=s1 (output of mux1 is one input)
    else if (s2) out = c;   // mux3: sel=s2 (output of mux2 is one input)
    else if (s3) out = d;   // mux4: sel=s3 (output of mux3 is one input)
end
// Synthesized: 3-level mux chain. s0 must propagate through ALL 3 muxes.
// Critical path: ~3 × mux_delay ≈ 750ps in 28nm
 
// Form B: unique if — synthesis generates parallel mux
always_comb begin
    out = '0;
    unique if (s0) out = a;   // Tool knows: only one can be true
    else if  (s1) out = b;   // → generates 4:1 one-hot mux
    else if  (s2) out = c;   // → ALL inputs evaluated in parallel
    else if  (s3) out = d;   // → 1-level mux
end
// Synthesized: 1-level parallel mux. ~250ps. 3× timing improvement.
// Bonus: simulator warns if two selects are high (spec violation caught)
 
// Form C: for truly overlapping signals, use priority if
always_comb begin
    out = '0;
    priority if (req[3]) out = d;  // highest priority
    else if     (req[2]) out = c;
    else if     (req[1]) out = b;
    else if     (req[0]) out = a;  // lowest priority
end
// Synthesized: priority encoder (4→2 binary) driving mux select
// ~log2(4) = 2 levels. Better than plain if chain.

🚀 RTL Design Insight: Use unique if for All One-Hot and Enum Decodes

The most common use case for unique if in production RTL is decoding FSM state registers, opcode fields, and one-hot encoded control signals. All of these are architecturally mutually exclusive — only one state/opcode is active at a time. Replacing plain if-else if with unique if in these cases gives you: (1) synthesis QoR improvement from parallel mux generation, (2) simulation safety net catching FSM encoding bugs, and (3) lint tool sign-off improvement since most lint rules require unique if or unique case for one-hot decodes.

🔍 Latch Inference from if — Step-by-Step Debugging

Latch inference from an incomplete if in always_comb is one of the most common RTL bugs. With always_comb, the tool catches it immediately (error at compile time). With legacy always @(*), the latch is silently created — often not discovered until post-synthesis simulation fails.

  1. **** — See the tool error: "always_comb block infers latch on signal 'out'". With always @(*), this may be a WARNING only — treat it as an error.
  2. **** — Find every path through the block. Draw a tree: for each if branch and each else if, list what out is assigned to. Include the implicit "no branch taken" path.
  3. **** — Identify the missing path. The latch is inferred on the path where out has no assignment. This is always: a missing else, a missing default, or a signal omitted from a branch.
  4. **** — Fix with default assignment at top: out = '0; as the very first line of the always_comb block. This single line covers ALL paths — latch disappears.
  5. **** — Run regression. The default changes functional behavior for the previously-unhandled path. Any test that relied on the old hold behavior will now fail — which is the correct outcome (the test was hiding the bug). ❌ Three ways to accidentally infer a latch// Bug 1: if without else always_comb begin if (en) out = data; // en=0 → out not assigned → LATCH end // Bug 2: case without default always_comb begin if (s==2'b00) out=a; else if (s==2'b01) out=b; // s=10 or 11 → LATCH end // Bug 3: nested if misses one signal always_comb begin if (mode) begin x = a; if (en) y = b; // mode=1, en=0 → y not assigned → LATCH on y end else begin x = '0; y = '0; end end✅ All three fixed with top-level defaults// Fix 1: default covers all paths always_comb begin out = '0; // ← covers en=0 if (en) out = data; end // Fix 2: default covers s=10,11 always_comb begin out = '0; // ← covers all if (s==2'b00) out=a; else if (s==2'b01) out=b; end // Fix 3: defaults for ALL outputs always_comb begin x = '0; y = '0; // ← covers everything if (mode) begin x = a; if (en) y = b; // en=0: y stays '0 (from default) end end

⚙ Advanced Code Examples — Industry-Grade RTL Patterns

Example A — AXI-Lite Address Decoder (unique if)

SystemVerilog — AXI-Lite Address Decoder using unique if
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── AXI-Lite slave address decoder ───────────────────────────────
// Address regions are mutually exclusive — unique if is correct
module axi_lite_decoder #(
    parameter logic [31:0] CTRL_BASE  = 32'h0000_0000,
    parameter logic [31:0] DATA_BASE  = 32'h0001_0000,
    parameter logic [31:0] STAT_BASE  = 32'h0002_0000,
    parameter logic [31:0] REGION_MSK = 32'hFFFF_0000
) (
    input  logic [31:0] awaddr,
    output logic        ctrl_sel, data_sel, stat_sel, err_sel
);
    always_comb begin
        // Default: error — address hits no valid region
        {ctrl_sel, data_sel, stat_sel, err_sel} = 4'b0001;
 
        unique if ((awaddr & REGION_MSK) == CTRL_BASE) begin
            ctrl_sel = 1; err_sel = 0;
        end else if ((awaddr & REGION_MSK) == DATA_BASE) begin
            data_sel = 1; err_sel = 0;
        end else if ((awaddr & REGION_MSK) == STAT_BASE) begin
            stat_sel = 1; err_sel = 0;
        end
        // No else needed — default covers error case
        // unique if: simulation warns if two regions decode simultaneously
        // (would indicate a parametrization error)
    end
endmodule
 
// ── Verification: check decoder with directed test ─────────────────
module tb_decoder;
    logic [31:0] awaddr;
    logic        ctrl_sel, data_sel, stat_sel, err_sel;
 
    axi_lite_decoder u_dut (.awaddr,.ctrl_sel,.data_sel,.stat_sel,.err_sel);
 
    initial begin
        awaddr = 32'h0000_0004; #1;
        assert(ctrl_sel && !err_sel) else $error("CTRL decode fail");
 
        awaddr = 32'h0001_0020; #1;
        assert(data_sel && !err_sel) else $error("DATA decode fail");
 
        awaddr = 32'h0003_0000; #1;  // unknown region
        assert(err_sel)             else $error("ERR decode fail");
        $finish;
    end
endmodule

Example B — Power Management FSM (priority if for overlapping power events)

SystemVerilog — Power Management Controller with priority if
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Power state arbiter: multiple power events can occur simultaneously
// Critical system event (thermal) must always win over user request
typedef enum logic [1:0] {
    PWR_FULL   = 2'b00,
    PWR_REDUCE = 2'b01,
    PWR_SLEEP  = 2'b10,
    PWR_OFF    = 2'b11
} pwr_state_t;
 
module pwr_ctrl (
    input  logic thermal_alert,   // hardware thermal sensor — HIGHEST priority
    input  logic battery_low,     // battery monitor
    input  logic user_sleep_req,  // software request
    input  logic user_wake_req,   // software request
    output pwr_state_t pwr_cmd
);
    always_comb begin
        pwr_cmd = PWR_FULL;         // default: full power
 
        priority if (thermal_alert) begin
            pwr_cmd = PWR_SLEEP;   // thermal: always overrides everything
        end else if (battery_low) begin
            pwr_cmd = PWR_REDUCE;  // battery: overrides user, not thermal
        end else if (user_sleep_req && !user_wake_req) begin
            pwr_cmd = PWR_SLEEP;
        end else if (user_wake_req) begin
            pwr_cmd = PWR_FULL;
        end
        // thermal_alert + battery_low + user_sleep all asserted simultaneously:
        // priority if → thermal wins, pwr_cmd = SLEEP. No simulation warning.
        // This is intentional design — priority if is the correct modifier.
    end
endmodule

Example C — Verification Scoreboard using if-else (RTL correctness check)

SystemVerilog — Testbench Scoreboard using if-else for ALU Checking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── ALU scoreboard: uses if-else to compute reference model output
module alu_scoreboard;
    logic [7:0] a, b, dut_result;
    logic [3:0] op;
    logic        valid;
    int          pass_cnt = 0, fail_cnt = 0;
 
    // Reference model: uses unique if — op values are mutually exclusive
    function automatic logic [8:0] alu_ref(
        input logic [7:0] a, b,
        input logic [3:0] op
    );
        unique if  (op == 4'h0) return {1'b0, a} + {1'b0, b};   // ADD
        else if    (op == 4'h1) return {1'b0, a} - {1'b0, b};   // SUB
        else if    (op == 4'h2) return {1'b0, a & b};           // AND
        else if    (op == 4'h3) return {1'b0, a | b};           // OR
        else if    (op == 4'h4) return {1'b0, a ^ b};           // XOR
        else if    (op == 4'h5) return {1'b0, ~a};             // NOT
        else                    return 9'h000;                  // unhandled
    endfunction
 
    // Scoreboard: sample DUT output, compare with reference
    always @(posedge valid) begin
        automatic logic [8:0] expected = alu_ref(a, b, op);
        if (dut_result === expected[7:0]) begin
            pass_cnt++;
        end else begin
            $error("FAIL: op=%0h a=%0h b=%0h got=%0h exp=%0h",
                    op, a, b, dut_result, expected[7:0]);
            fail_cnt++;
        end
    end
endmodule

🔬 Debugging Academy — 8 Real if-else Bugs from the Field

Every one of these bugs has appeared in real RTL projects or code reviews. The symptoms look confusing until you understand exactly how the simulator and synthesis tool interpret the code.

1

unique if flooding the simulation log with overlap warnings

SIM WARNING STORM
Buggy Code
Bug 1 — Wrong Modifier for Overlapping Conditions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: IRQ lines can be simultaneously asserted — unique if is wrong here
always_comb begin
    irq_id = 3'b000;
    unique if (irq[0]) irq_id = 3'd0;
    else if   (irq[1]) irq_id = 3'd1;
    else if   (irq[2]) irq_id = 3'd2;
end
// Simulation: 50,000 lines of "unique if overlap violation" in log
// Every clock cycle where 2+ IRQs are pending → warning
// Engineers suppress all warnings → real bugs start getting missed
 
// ✅ FIX: use priority if — overlap is intentional for IRQ arbiters
always_comb begin
    irq_id = 3'b000;
    priority if (irq[0]) irq_id = 3'd0;  // highest priority
    else if     (irq[1]) irq_id = 3'd1;
    else if     (irq[2]) irq_id = 3'd2;
end
Root Cause

unique if asserts that the conditions are mutually exclusive. IRQ signals by definition can overlap — multiple interrupt sources fire simultaneously, and that is the controller's normal operating mode, not a fault. So the simulator issues a violation report on every cycle where two or more IRQ bits are set.

Real Project Impact

Warning logs fill with thousands of lines per run. Engineers add suppression flags to silence them, and those flags also suppress genuine unique if violations elsewhere in the design — so the mechanism that was supposed to catch one-hot bugs stops reporting them. This is warning fatigue, and it is a verification-quality problem rather than a cosmetic one: the team ends up worse off than if the modifier had never been used.

Fix

Replace unique if with priority if. The generated hardware is equivalent — the first matching branch still wins — but the simulator no longer reports overlap, which was expected behaviour all along. The genuinely useful check survives: priority if still reports when no branch is taken, which for an interrupt controller means the arbiter ran with no pending request.

2

Latch inferred — output holds a stale value and the directed test still passes

LATCH INFERENCE
Buggy Code
Bug 2 — Latch from Incomplete if in always_comb
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: FIFO read data path — output not assigned when not reading
always_comb begin
    if (rd_en && !empty) begin
        rd_data = fifo_mem[rd_ptr];
        rd_valid = 1'b1;
    end
    // ❌ rd_data, rd_valid not assigned when !rd_en or empty
    // → LATCH inferred on rd_data AND rd_valid
    // Directed test: only reads when fifo is ready → passes
    // Random test: reads empty fifo → stale rd_data from previous read
end
 
// ✅ FIX: default assignment covers all paths
always_comb begin
    rd_data  = '0;         // safe default: don't expose stale data
    rd_valid = 1'b0;       // valid=0 when not reading
    if (rd_en && !empty) begin
        rd_data  = fifo_mem[rd_ptr];
        rd_valid = 1'b1;
    end
end
Waveform Symptom

rd_data holds its last value when rd_en deasserts. On the waveform it looks like a register — steady between reads — even though it was declared logic and lives in an always_comb block. The inferred latch is what gives it apparent memory, and the downstream module consumes that stale data as if it were fresh.

Why the Directed Test Passes

The directed test only ever reads valid data from a non-empty FIFO. It never deasserts rd_en mid-stream and never reads an empty FIFO, so the one value the latch is holding happens to be the correct one on every cycle the test checks. A constrained-random test that deasserts rd_en at an arbitrary point, or reads an empty FIFO, exposes it on the first attempt.

Fix

Assign every output unconditionally at the top of the block, before any if. A default of '0 for the data and 1'b0 for the valid flag both removes the latch and makes the failure mode safe: a consumer that ignores rd_valid now sees zeros rather than a plausible-looking stale payload.

3

Priority inverted — a critical condition silently loses to a lower-priority one

WRONG PRIORITY
Buggy Code
Bug 3 — Wrong Branch Order Inverts Priority
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: thermal alert is highest priority, but listed LAST
always_comb begin
    pwr_state = PWR_FULL;
    priority if (user_req)      pwr_state = PWR_SLEEP;  // low priority, listed first
    else if     (battery_low)   pwr_state = PWR_REDUCE;
    else if     (thermal_alert) pwr_state = PWR_OFF;    // ❌ critical — listed LAST
end
// When thermal_alert AND user_req are both asserted:
// → user_req (first branch) wins → PWR_SLEEP
// → Chip overheats because thermal_alert is silently ignored
// This is a SAFETY BUG — silicon may be damaged
 
// ✅ FIX: highest priority first
always_comb begin
    pwr_state = PWR_FULL;
    priority if (thermal_alert) pwr_state = PWR_OFF;    // ✅ critical first
    else if     (battery_low)   pwr_state = PWR_REDUCE;
    else if     (user_req)      pwr_state = PWR_SLEEP;  // lowest priority last
end
Why This Is Hard to Find

The code reads as logically correct: every condition is handled and nothing is missing. The defect is entirely in the ordering, which no lint rule and no review checklist catches unless the reviewer knows the intended priority. In directed tests that never assert two conditions at once, every test passes. Only a stimulus that raises user_req and thermal_alert in the same cycle exposes it — which is exactly the kind of coincidence constrained-random generates naturally and directed tests almost never do.

Rule

In any priority if or if-else if chain, list the highest-priority condition first, and state the intended order in a comment at the top of the block so a reviewer can check the code against an explicit specification rather than against their own assumption. For a safety-relevant chain, back it with an assertion: assert property (@(posedge clk) thermal_alert |-> pwr_state == PWR_OFF) fails the moment the ordering regresses, which a comment cannot do.

4

Assignment (=) written instead of comparison (==) in an if condition

SYNTAX/LOGIC BUG
Buggy Code
Bug 4 — Assignment (=) Instead of Comparison (==) in Condition
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: = instead of == in if condition
always_comb begin
    out = '0;
    if (state = IDLE)   // ❌ ASSIGNMENT, not comparison!
        out = idle_data;  // state is now ALWAYS set to IDLE (1)
    else if (state == BUSY)
        out = busy_data;  // this branch is NEVER taken (state=IDLE always)
end
// In always_comb: = is a blocking assignment inside the block
// Condition evaluates as the VALUE assigned (IDLE = non-zero = true)
// state gets overwritten to IDLE on every evaluation
// Synthesis: state is driven from multiple sources → multi-driver error
 
// ✅ FIX: use == for comparison
always_comb begin
    out = '0;
    if (state == IDLE)    // ✅ comparison — state is not modified here
        out = idle_data;
    else if (state == BUSY)
        out = busy_data;
end
Root Cause

Writing = inside an if condition in an always_comb block is a blocking assignment, not a comparison. The condition then evaluates to the value assignedIDLE, which is non-zero and therefore true — so the first branch is taken unconditionally and state is overwritten on every evaluation of the block. The later state == BUSY branch becomes unreachable. Synthesis usually catches this as a multi-driver error on state, because the block is now driving a signal that a sequential block also drives, but simulation runs happily and produces a design that always reports IDLE.

Fix

Use ==. The broader defence is that this is one of the few bugs a linter reliably catches — an assignment inside a condition is a standard rule in every lint ruleset, so the real lesson is that the rule should be enabled and its warnings treated as errors rather than triaged.

5

Reset is not the first branch, so an enable can gate the reset

RESET PRIORITY BUG
Buggy Code
Bug 5 — Reset Buried Inside always_ff — Not Always Effective
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: reset is NOT the first branch — gated by enable
always_ff @(posedge clk or negedge rst_n) begin
    if      (en)     q <= d;      // ❌ en checked first!
    else if (!rst_n) q <= '0;   // reset only fires when !en!
end
// If en=1 and rst_n=0 simultaneously: en branch wins → q is NOT reset
// This is a timing-sensitive functional bug
// Synthesis: reset path is conditional — STA may not apply reset timing
 
// ✅ FIX: reset ALWAYS first — unconditional
always_ff @(posedge clk or negedge rst_n) begin
    if      (!rst_n) q <= '0;   // ✅ reset first — fires regardless of en
    else if (en)     q <= d;
end
 
// Rule: In always_ff with async reset:
// if (!rst_n) is ALWAYS the first, unconditional branch. No exceptions.
Root Cause

With en tested first, the reset branch is only reachable when en is low. If en is high while rst_n falls, the flop takes d instead of resetting. That is a functional bug on its own, but the synthesis consequence is worse: the reset is no longer an unconditional asynchronous path, so the tool may not recognise it as a reset at all. It then infers ordinary logic on the asynchronous input, and static timing analysis never applies reset recovery and removal checks to it.

Fix

In an always_ff with an asynchronous reset, if (!rst_n) is the first branch, unconditionally, with no exceptions. Every synthesis tool pattern-matches this shape to infer the reset flop; anything else is a gamble on the tool's inference.

6

A nested if leaves only the inner output latched

NESTED LATCH
Buggy Code
Bug 6 — Nested if: Outer Default Does Not Cover Inner Paths
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: partial inner coverage — flag_b gets a latch
always_comb begin
    flag_a = 1'b0;        // outer default — covers flag_a everywhere
    flag_b = 1'b0;        // outer default — but does it cover inner paths?
    if (mode == WRITE) begin
        flag_a = 1'b1;
        if (burst) flag_b = 1'b1;  // ✅ flag_b assigned when burst=1
        // ← flag_b when mode=WRITE, burst=0: NOT re-assigned here
        // ← BUT outer default already set flag_b=0 before entering if!
        // ← So: flag_b=0 via DEFAULT, not via latch — NO LATCH in this case
    end
end
// Actually this is CORRECT — outer default covers inner paths.
// The trap: if you remove the outer default for flag_b:
 
always_comb begin
    flag_a = 1'b0;         // ❌ no default for flag_b
    if (mode == WRITE) begin
        flag_a = 1'b1;
        if (burst) flag_b = 1'b1;  // flag_b only assigned in one path
        // mode=WRITE, burst=0: flag_b never assigned → LATCH!
        // mode=READ: flag_b never assigned → LATCH!
    end
end
// FIX: add flag_b = 0 at the very top of the block (see above correct version)
Root Cause

The trap is that the outer default is easy to read as covering the whole block when it only covers the signals it actually names. In the first version both flag_a and flag_b get defaults, so the inner if (burst) is safe and no latch appears — the code is correct. Remove flag_b's default and only the inner path assigns it, so mode == WRITE with burst low, and every value of mode other than WRITE, leave it unassigned and a latch is inferred on flag_b alone. flag_a remains clean, which is what makes the bug confusing: half the block behaves and half does not.

Fix

Give every output a default at the very top of the block, before any conditional — not inside the outermost if, and not per-branch. always_comb will report the latch, but only for signals it can prove are incompletely assigned, so the habit is worth more than the tool check.

7

unique if reporting violations during reset because the state is X

X-STATE / RESET PHASE
Buggy Code
Bug 7 — unique if During Reset Phase Produces Spurious X Warnings
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: state register is X during reset → unique if fires warnings
typedef enum logic [1:0] {IDLE=2'b00, BUSY=2'b01, DONE=2'b10} st_t;
st_t state;
 
always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) state <= IDLE; else state <= next_st;
 
always_comb begin
    next_st = state;
    unique if (state == IDLE) next_st = start ? BUSY : IDLE;
    else if  (state == BUSY) next_st = done  ? DONE : BUSY;
    else if  (state == DONE) next_st = IDLE;
end
// At T=0 (before reset): state=2'bXX
// unique if evaluates: (XX==00)=X, (XX==01)=X, (XX==10)=X
// None are true → "unique if: no condition true" warning fires
// Log shows hundreds of warnings before simulation even starts
 
// ✅ FIX: add explicit X/default handling
always_comb begin
    next_st = IDLE;              // default: safe state if X
    unique if (state == IDLE) next_st = start ? BUSY : IDLE;
    else if  (state == BUSY) next_st = done  ? DONE : BUSY;
    else if  (state == DONE) next_st = IDLE;
    else                     next_st = IDLE;  // covers X state
end
// The else branch prevents "no condition true" warning when state=X
Root Cause

Before reset takes effect, state is 2'bXX. Each comparison against an enum value evaluates to X rather than to true or false, so no branch is selected and unique if reports "no condition is true" on every evaluation. The reports are technically accurate and completely useless — they describe the reset window, not a design fault, and they arrive in the hundreds before the simulation reaches any interesting behaviour.

Note that disable iff is not available here: this is a violation report on a procedural statement, not a concurrent assertion, so there is no clocking or disable construct to attach.

Fix

Give the chain a final else so that some branch is always taken. That resolves the no-match report while leaving the uniqueness check intact for the cases that matter, and it also makes the X behaviour explicit — an unknown state now lands in a defined recovery state instead of holding whatever it held.

unique0 if is the other correct answer, and often the better one: it checks that at most one condition is true and does not require that any branch be taken, which is precisely the semantics wanted here. Reach for it whenever the conditions are genuinely exclusive but incomplete.

8

A monitor in the wrong scheduling region prints a value that lags by a cycle

DELTA CYCLE ORDERING
Buggy Code
Bug 8 — $display in always_comb Sees Wrong Value Due to Delta Cycles
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: monitoring combinational logic with $display — wrong values
always_ff @(posedge clk) q <= d;
 
always_comb begin
    if (q == 8'hA5) comb_flag = 1'b1;
    else             comb_flag = 1'b0;
end
 
// ❌ Bug: monitoring at posedge clk — sees BEFORE or AFTER NBA update?
always @(posedge clk) begin
    $display("T=%0t q=%h flag=%b", $time, q, comb_flag);
    // PROBLEM: $display fires in Active region BEFORE non-blocking (NBA) updates
    // Shows OLD q, OLD comb_flag — looks like a 1-cycle lag in monitoring
end
 
// ✅ FIX: use $strobe — fires in Postponed region AFTER all NBA updates
always @(posedge clk) begin
    $strobe("T=%0t q=%h flag=%b", $time, q, comb_flag);
    // $strobe: fires after Active→NBA→Delta convergence → shows correct values
end
Root Cause

$display executes in the Active region, and the non-blocking assignment to q updates in the NBA region, which runs later in the same time step. So the $display in an @(posedge clk) block prints the value q held before the edge, along with the comb_flag derived from it. Nothing is wrong with the design; the monitor is sampling at the wrong point in the time step.

The Real Lesson

This one costs disproportionate time because it makes correct logic look broken. The printed trace shows comb_flag changing a cycle after q, which reads exactly like an accidental pipeline register, so engineers start hunting for a flop that does not exist.

$strobe schedules in the Postponed region, after all NBA updates for that time step have settled, so it prints the values the design actually converged on. Use it for any monitor that observes signals derived from non-blocking assignments. A clocking block in the testbench solves the same problem structurally, which is why UVM environments rarely hit this at all — see race conditions and determinism for the scheduling model this rests on.

💡 Senior Verification Engineer Tip: Enable Unique/Priority Checks Globally

In VCS, add +define+SV_UNIQUE_PRIORITY_CHECK to ensure unique if and priority if runtime checks are always active. In Questa, checks are enabled by default but can be controlled with -sv_unique and -sv_priority. Many teams accidentally disable these checks with overly broad warning suppress flags — then wonder why their simulations aren't catching conditions the modifiers were meant to catch. Audit your simulation command line: if you see broad warning suppression flags, that is a red flag for your verification quality.

🎯 Interview Q&A — From Fresher to Senior RTL Engineer

An if-else if chain inside always_comb infers a priority mux chain — a series of cascaded 2:1 muxes, with each condition becoming the select line of one mux. The first true condition's output then passes through all the later muxes unchanged. Because the chain is structurally serial, a four-level chain puts four mux delays in the critical path, which is why long if-else if chains hurt timing: every condition you add extends the path by roughly one mux delay. That cost is invisible in RTL simulation and shows up only in synthesis reports, so it tends to be discovered late.

unique if declares that the conditions are mutually exclusive — at most one can be true at a time. The simulator reports a violation if two conditions are simultaneously true (an overlap violation) or if no condition is true and there is no final else (a no-match violation). Synthesis is free to build a parallel mux.

priority if declares that conditions may overlap and that the first matching branch intentionally wins. The simulator reports only the no-match case; overlap is expected and goes unreported. Synthesis builds an optimised priority encoder.

The rule of thumb: unique if for one-hot and enum decodes, where overlap really would be a bug; priority if for interrupt arbiters, power-management chains and anything else where several sources can legitimately fire at once. Choosing unique for the second class is the single most common misuse, and it is the subject of Debug Lab 1.

Because always_comb requires every output to be assigned on every path through the block, and an if-else if chain without a final else leaves at least one path where nothing is assigned. On that path the signal must hold its previous value, which is memory — so the tool infers a latch.

A single default assignment at the very top of the block, before any conditional, covers every path including the combinations you did not think to handle. always_comb will report the incomplete assignment where it can prove one, but the default is worth writing regardless: it also decides what the safe value is, which the latch never does. Legacy always @(*) gives no error at all and simply creates the latch silently, which is why so much older RTL carries them. See procedural blocks for the inference rules in full.

With plain if-else if, the tool does not know that only one condition can be true, so it must generate a priority chain that would still behave correctly if several were true simultaneously. That is N cascaded 2:1 muxes in series — the worst-case timing structure available.

unique if supplies the missing guarantee. The tool can now build a one-hot parallel mux: all inputs are evaluated at once and the single active select routes the result directly, collapsing N mux delays to roughly one. For a wide decode this is a large recovery — wide opcode decodes and bus decoders regularly fail timing on the priority-chain form and close on the parallel form with no change to the logic itself.

Two caveats worth stating in an interview. The guarantee is a promise you are making, not something the tool verifies at elaboration — if the conditions can overlap in silicon, you have told the tool something false and the hardware may not match simulation. And modern tools often infer the parallelism anyway from don't-care analysis, so the more dependable value of unique is the simulation check rather than the QoR.

Almost certainly unique if used where the conditions legitimately overlap — an interrupt controller, an arbiter, or a power-management block where several request signals can assert at once. The fix is priority if: the hardware is identical, the first matching branch still wins, and overlap is no longer reported while the useful no-match check is retained.

The thing not to do is suppress the warnings. A global suppression also hides genuine unique if violations everywhere else in the design, converting a working check into a silent one — and the places where unique is used correctly are exactly the places where a violation would have been worth knowing about. Fix the modifier at the source rather than filtering its output.

Yes, it is legal, and the violation checks apply exactly as they do in always_comb. The synthesis effect is that the mux unique permits now drives the D input of the flop, so the benefit is reduced logic depth on the D path and better setup timing — useful when decoding one-hot FSM states.

The thing to watch for is the no-match check, and it catches people out constantly. Consider a reset-and-enable flop written as unique if (!rst_n) q <= '0; else if (en) q <= d;. Whenever rst_n is high and en is low — which is the normal idle condition of most flops in most designs — no branch is taken and the simulator reports a violation. The chain is not wrong as hardware; it is simply incomplete, and unique demands completeness as well as exclusivity.

There are three correct responses. Add a final else if a default action exists. Use unique0 if, which checks exclusivity but does not require a branch to be taken, and is the right modifier whenever the conditions are exclusive but deliberately incomplete. Or use plain if, which is entirely appropriate for a reset-enable flop where the priority is intentional and there is nothing to check.

A simulation-synthesis mismatch caused by the tool optimising on an assumption the RTL never stated. In RTL simulation the if-else if chain evaluates serially, so if two conditions are ever simultaneously true — which should not happen for a genuine one-hot, but can if the one-hot generator is buggy or if the signal is glitching through a real timing path — the first one wins deterministically. Synthesis, meanwhile, may have inferred the one-hot property from don't-care analysis and built a structure that does something different in that same situation.

The fix is to state the constraint explicitly with unique if, which addresses both halves at once: the simulator now reports if the one-hot assumption is ever violated, and the tool has explicit permission to optimise rather than an inference it made on its own. The deeper point for an interview is that the mismatch was never really about if — it was about an assumption held by the tool and not by the source, and making assumptions explicit is what these modifiers are for.

A 16-level if-else if chain generates fifteen cascaded 2:1 muxes. Taking a 2:1 mux at roughly 150–200 ps in a 28 nm library, the chain contributes on the order of 2.5 ns. At 1 GHz the period is 1 ns, and after setup and clock uncertainty there is perhaps 900 ps of combinational budget — so the priority chain overruns the budget by nearly three times and the path cannot close.

With unique if the tool builds a parallel 16:1 mux: one mux delay plus the decode, in the region of 300–400 ps, comfortably inside budget. This is a routine finding rather than a hypothetical — wide opcode decodes in processor datapaths and address decoders in bus fabrics fail timing on the chain form regularly, and switching the modifier fixes them without touching the logic.

The number to carry into an interview is the shape rather than the picoseconds: a priority chain is O(N) in logic depth and a parallel mux is O(1), so the gap widens with every condition you add. That is also why the fix is worth the most on exactly the decodes that are hardest to restructure by hand.

Where This Is Specified

  • IEEE 1800-2023 (SystemVerilog) §12.4.2 — unique-if, unique0-if, and priority-if. The decision-modifier semantics, and the two classes of violation report: more than one condition true, and no condition true with no final else. unique0 relaxes only the second.
  • IEEE 1800-2023 §12.4 — Conditional if-else statement. Evaluation order and the priority behaviour of an unmodified chain.
  • IEEE 1800-2023 §9.2.2.2 — always_comb. The complete-assignment requirement whose violation infers a latch.
  • IEEE 1800-2023 §4.9 and §10.4.2 — Scheduling semantics and nonblocking assignment. The Active/NBA/Postponed ordering that Debug Lab 8 rests on, and why $strobe observes the settled values that $display cannot.

Part of SystemVerilog Fundamentals·Procedural Statements·Lesson 30 of 53

View program

Continue learning