Verilog bug catalog

Inferred Latches in Verilog: Causes, Detection, and Fixes

An inferred latch is what synthesis produces when a combinational block leaves an output undriven on some control path. The synthesizer is forced to remember the previous value, so it inserts a level-sensitive latch you never asked for. Inferred latches cause glitchy timing, surprise hold-time paths that static timing analysis does not constrain correctly, and intermittent failures in silicon. They are almost always a bug. This page covers every pattern that produces them — incomplete if/else, incomplete case, partial vector assignment, and FSM next-state gaps — the code that fixes each, the difference between a latch and a flip-flop, when an intentional latch is the right call, and how to detect them across an entire repository in CI before they reach a tapeout.

What is an inferred latch?

A latch is a level-sensitive memory element. A flip-flop is edge-sensitive. Both store state, but you only want a flip-flop in synchronous RTL. When you write a block that is supposed to be combinational and the synthesizer cannot prove that every output is assigned on every path, it builds a latch around the output to hold the previous value. Yosys reports this as Latch inferred for signal `\foo` during the proc pass. Design Compiler emits HDL-220. Verilator emits LATCH (off by default).

Latch vs flip-flop vs clock gate

These three get conflated, and the confusion is what lets inferred latches survive review. They are distinct primitives:

  • Latch (level-sensitive). Transparent while its enable is high; holds when the enable goes low. There is no clock edge. This is what gets inferred from an incomplete combinational block.
  • Flip-flop (edge-sensitive). Samples its input only on a clock edge and holds for the rest of the cycle. This is what you want in synchronous RTL, and you get it from always_ff or always @(posedge clk).
  • Clock gate. An AND/latch structure that suppresses the clock to a bank of flip-flops to save power. The flops still store on an edge; the gate just decides whether the edge arrives. A clock gate is a deliberate, characterised cell, not a memory element in its own right.

An inferred latch is none of these by intent. It appears in a block you meant to be purely combinational, which is exactly why it is a surprise: you wrote what looks like a mux and got a memory element.

Why it happens

Every cause reduces to the same root: a signal read combinationally that is not written on at least one reachable path. These are the high-frequency shapes that bug takes:

  • A case statement without a default and no full coverage of the selector.
  • An if without a matching else, where the body assigns a signal that is not assigned outside the if.
  • A partial vector assignment: you drive flags[1:0] but never flags[3:2], so the undriven bits latch. The missing-else rule applied per bit.
  • An FSM next-state or datapath signal left unassigned in one state arm. The single most common real-world source — see the FSM example below.
  • Non-blocking assignments inside what should be combinational logic (an always @(*) or always_comb), because non-blocking semantics in a comb block can leave a signal holding state across deltas. (always_comb with <= is itself a tool warning, but legacy always @(*) blocks slip through.)

Example: missing case default

Classic 4-way mux with only three branches. The synthesizer cannot prove what next is when sel == 2'b11, so it builds a latch.

// BUG: missing default branch -> 'next' is not assigned when sel == 2'b11
module mux_bug (
    input  logic [1:0] sel,
    input  logic [7:0] a, b, c,
    output logic [7:0] next
);
    always_comb begin
        case (sel)
            2'b00: next = a;
            2'b01: next = b;
            2'b10: next = c;
        endcase
    end
endmodule

The fix is to assign next on every path. The cleanest pattern is an unconditional pre-assignment at the top of the block, then override under the case. A default branch is good practice even when the case appears full, because x propagation in simulation will otherwise hide the bug.

// FIX: assign 'next' on every path. Either default, or unconditional pre-assign.
module mux_fixed (
    input  logic [1:0] sel,
    input  logic [7:0] a, b, c,
    output logic [7:0] next
);
    always_comb begin
        next = '0;                 // unconditional default kills the latch
        case (sel)
            2'b00: next = a;
            2'b01: next = b;
            2'b10: next = c;
            default: next = '0;    // belt and suspenders
        endcase
    end
endmodule

Example: missing else

The same rule applies to if. If you assign y only when enable is high, the synthesizer must remember the prior value of y when enable is low.

// BUG: 'y' is only driven on the 'true' branch
always_comb begin
    if (enable)
        y = data_in;
    // no else -> latch on y
end
// FIX: provide every signal on every branch
always_comb begin
    if (enable)
        y = data_in;
    else
        y = '0;
end

Example: partial vector assignment

The latch rule applies bit by bit. If you assign some bits of a vector and leave others untouched on a given path, only the untouched bits latch. This is insidious because most tools report the latch at signal granularity — they will name flags without telling you it is only flags[3:2] that latched, so the line you wrote looks fully assigned at a glance. Be careful not to confuse this with a width mismatch: assigning a too-narrow expression to the whole vector simply zero- or sign-extends (or truncates) the value — it does not latch. A latch only appears when the assignment is partial and path-dependent: some bits or indices are driven on some paths and left unassigned on others.

// BUG: only some bits of 'flags' are assigned, so the rest latch.
// This is the same rule as a missing else, applied per-bit. Tools that
// report at signal granularity will name 'flags' but not the bit range.
module status_bug (
    input  logic       err,
    input  logic       busy,
    output logic [3:0] flags
);
    always_comb begin
        flags[0] = err;
        flags[1] = busy;
        // flags[2] and flags[3] are never assigned -> 2-bit latch
    end
endmodule

The fix is the same canonical pattern as everywhere else: drive the whole vector unconditionally at the top of the block, then override the bits you care about.

// FIX: drive the whole vector unconditionally first, then override.
module status_fixed (
    input  logic       err,
    input  logic       busy,
    output logic [3:0] flags
);
    always_comb begin
        flags    = '0;     // every bit gets a default
        flags[0] = err;
        flags[1] = busy;
    end
endmodule

Example: the FSM next-state latch

In practice, the single most common place an inferred latch hides is the combinational next-state block of a finite state machine. Each case arm tends to assign a different subset of the next-state and datapath signals, and it is easy to forget one signal in one state. Every signal that is not assigned in every arm latches. Here both next_state (unassigned in the ERR arm) and count_next (assigned only in RUN) infer latches.

// BUG: classic FSM next-state latch. 'next_state' is unassigned in the
// ERROR state, and 'count_next' is unassigned unless we are in RUN.
// Both latch. FSMs are the #1 real-world source of inferred latches.
typedef enum logic [1:0] {IDLE, RUN, DONE, ERR} state_e;

always_comb begin
    case (state)
        IDLE: next_state = start ? RUN : IDLE;
        RUN:  begin
            next_state = done ? DONE : RUN;
            count_next = count + 1'b1;
        end
        DONE: next_state = IDLE;
        // ERR has no next_state assignment -> latch
        // count_next only assigned in RUN     -> latch
    endcase
end

The disciplined fix — and the reason the two-always-block FSM style is so widely taught — is to give every next-state and datapath signal an unconditional default at the top of the block. Defaulting next_state = state also documents the “hold this state” behaviour explicitly instead of leaving it to an accidental latch.

// FIX: default every next-state and datapath signal at the top of the
// block. This is the canonical two-always FSM combinational style.
always_comb begin
    next_state = state;        // default: hold current state explicitly
    count_next = count;        // default: hold the counter explicitly
    case (state)
        IDLE: if (start)      next_state = RUN;
        RUN:  begin
            count_next = count + 1'b1;
            if (done)         next_state = DONE;
        end
        DONE:                 next_state = IDLE;
        ERR:                  next_state = IDLE;
        default:              next_state = IDLE;
    endcase
end

Example: non-blocking in combinational

The third pattern is more subtle. Putting <= in a combinational always block delays the LHS update to the NBA scheduling region. If the same block reads the LHS later in the same time step, it sees the previous value. The synthesizer cannot reproduce that delay in pure combinational logic, so it inserts a latch to hold the previous value across the time step. This is covered in detail in our blocking vs non-blocking article.

// BUG: '<=' in a comb block leaves 'next' holding state
always_comb begin
    if (load) next <= load_val;
    else      next <= next + 1;   // reads NBA-delayed self -> latch
end
// FIX: blocking in combinational
always_comb begin
    if (load) next = load_val;
    else      next = curr + 1;
end

What about intentional latches?

A small fraction of the time you really do want a latch: hold a configuration register through a multi-cycle interface, build a retiming pipeline in a clock-gated domain, or save power on a path that only switches a few times per second. The right way to do that is to write the latch explicitly. SystemVerilog provides always_latch for exactly this purpose. The block tells synthesis you meant to build a latch, and tells lint to suppress the warning. If you cannot write the equivalent always_latch form, you do not actually want a latch.

// Intentional latch: opcode is held while opcode_en is low
always_latch begin
    if (opcode_en) opcode_q = opcode_in;
end

Why simulation alone misses it

A latch is not a simulation error. A unit test that drives sel only to 0/1/2 will pass because the missing branch is never exercised. Even if you do hit the missing branch, the latch will simply hold the last value, which often happens to be the correct value by accident. Simulation can also propagate x from an uninitialised latch, but only if you actually look at the signal. What simulation will not do is produce a synthesis warning. The bug shows up at synthesis time, often months after the offending commit went in.

The always_comb safety net

The cheapest defence you can adopt today is to write every combinational block as always_comb rather than always @(*). always_comb does not prevent a latch — you can still leave a signal undriven — but IEEE 1800-2017 (clause 9.2.2.2) requires tools to report a latch inferred inside one. It converts a silent synthesis surprise into a compile-time warning that fires in simulation and lint, long before synthesis. It buys you two more guarantees that always @(*) does not:

  • The sensitivity list is computed automatically and includes every signal read, eliminating the missing-signal simulation/synthesis mismatch that @(*) can still hit through function calls.
  • Tools error if anything outside the block also writes the same signal, or if you use a blocking timing control inside it — both of which are latch-adjacent footguns.

The limitation: always_comb does nothing for the always @(*) blocks left over from older code, and the warning is still easy to lose in a noisy build log. That is why you also want a gate in CI — covered next.

How to detect it at scale

The right place to catch this is in CI, on every push, before synthesis runs. Four detectors work, in rough order of cost:

  • Verilator. verilator --lint-only -Wall enables the LATCH warning, which fires when a combinational process is missing an assignment. Promote it to fatal in CI with -Werror-LATCH. One nuance: a latch can also show up indirectly as UNOPTFLAT — a combinational loop where the signal feeds back into itself — because an unassigned-on-some-path signal that is also read in the same block looks like a cycle to the scheduler. Treat an UNOPTFLAT on a comb signal as a latch suspect, not just a performance note.
  • Yosys. Run to the proc pass and grep the log for "Latch inferred", or run read_verilog + prep and check for $dlatch cells in the netlist (stat). Authoritative because it is the actual synthesis frontend, but heavier and the message is buried in a long log.
  • Verible. The lint rule always-comb enforces that combinational blocks use always_comb (so the tool is obligated to report any latch), and case-missing-default flags the most common structural trigger before elaboration. See our Verible lint guide for the full rule set. Style-level, fast, no elaboration — but it cannot prove path coverage, only flag the shapes.
  • Type-aware AST analysis. Walk every combinational process, build the set of signals it writes per control path, and report any signal not assigned on every reachable path — including the per-bit partial-assignment case that line-level grep misses. This is the only approach that names the exact missing branch and bit range. It needs a parser that does not silently recover from syntax errors, or it will false-clean broken RTL.

Tools like ChipVerify AI's TinyTapeout scanner flag this rule as inferred_latch automatically and report the exact file:line, the signal name, and the missing branch. Point it at a GitHub URL and it will flag the rule-matched inferred latches it detects across the repo — a pre-signoff check, not an exhaustive proof — without you having to set up a synthesis flow.

Common false positives and edge cases

Not every "missing assignment" is a latch. A few patterns produce warnings that are not actually bugs and need to be suppressed rather than fixed:

  • A signal that is genuinely a register, written inside an always_ff, but visible from a parent module that triggers a comb-context check. Make sure the parent treats the port as registered.
  • A case with a unique or priority qualifier on a selector with a parameterised range. The synthesizer can sometimes see the case is full; the lint pass cannot.
  • A signal whose only assignment is inside a generate block that is conditionally instantiated. Treat as a configuration bug if any reachable configuration leaves the signal undriven.

Related rules

  • Blocking vs non-blocking assignment — using <= in a combinational block is one of the common causes of latch inference.
  • Output never assigned — the more degenerate cousin: the signal is never driven on any path, not just one.
  • Width mismatches — the related-but-distinct case: a too-narrow assignment to a whole vector zero-/sign-extends or truncates rather than latching; only a partial, path-dependent assignment leaves bits latched.
  • Clock domain crossing — an unconstrained inferred latch on a CDC path is doubly dangerous; STA does not close it and the data can be metastable.
  • Verible lint — the always-comb and case-missing-default rules catch the structural triggers before elaboration.
  • RTL verification — where lint, simulation, and structural checks fit together in a pre-synthesis flow.

FAQ

What causes an inferred latch in Verilog?

A combinational block (always_comb or always @*) that does not assign an output on every reachable control path. The most common causes are an if without an else, a case without a default (or without full selector coverage), and assigning only some bits of a vector. Synthesis must hold the previous value on the missing path, so it builds a level-sensitive latch.

Is an inferred latch always a bug?

Almost always, yes. There is a small set of legitimate latch uses such as low-frequency power islands, retention cells, and certain handshake interfaces. The rule is that a latch must be intentional, written with always_latch, and documented. An inferred latch is wrong because nobody asked for it and static timing analysis usually does not constrain it correctly.

How do I fix an inferred latch?

Assign the signal on every path. The cleanest pattern is an unconditional default assignment at the very top of the block, before any if or case, then override it conditionally. Equivalently, give every if an else and every case a default. Using always_comb instead of always @* makes the tool flag any remaining latch for you.

Does always_comb prevent inferred latches?

always_comb does not prevent a latch, but IEEE 1800 requires tools to flag a latch inferred from an always_comb block, so it turns a silent synthesis surprise into a compile-time warning. It also auto-computes the sensitivity list, eliminating a separate class of simulation/synthesis mismatch. You still have to drive every signal on every path.

What is the difference between a latch and a flip-flop?

A latch is level-sensitive: it is transparent while its enable is asserted and holds when the enable is deasserted. A flip-flop is edge-sensitive: it samples only on a clock edge. Synchronous RTL wants flip-flops (from always_ff or always @(posedge clk)). An inferred latch appears in a block you intended to be purely combinational, which is why it is a surprise.

Does Yosys catch every latch?

Yosys catches the structural cases reliably. It can miss latches inferred from generate blocks that are never elaborated, latches behind unbound parameters, and latches that synthesize away after constant propagation. The warning is also easy to lose in a long synthesis log, which is why a dedicated lint gate beats grepping synthesis output.

Why not just use a flop everywhere?

A flop costs more area than necessary and adds a clock load and one cycle of latency. Combinational logic is usually the right answer; the bug is that you wrote combinational logic that does not behave combinationally.

Scan your repo

Scan your repo for inferred latches: paste a GitHub URL at chipverify.ai/tinytapeout and you'll get back a list of every combinational signal that is not driven on every path, with file, line, and a suggested fix. No install, no synthesis license, no waiting on Design Compiler.

Find inferred latches in your design

Sign in and point ChipVerify AI at your Verilog or SystemVerilog. Its analyzer flags the inferred_latch rule with the exact file:line, signal, and missing branch — a pre-signoff structural check, not an exhaustive proof or a foundry signoff.