Four classic causes cover most accidental latches. Each leaves an output unassigned on some path, so the old value must be held.
Missing else leaves one path unassigned. When en is 0, nothing drives y.
// yosys: latch
module latch_if (input logic en, a, output logic y);
always_comb begin
if (en) y = a; // when en is 0, y is not assigned -> must hold
end
endmoduleThe synthesizer states the problem directly when you use always_comb:
ERROR: Latch inferred for signal `\latch_if.\y' from always_comb process `\latch_if.$proc$...'.That message comes from the open-source Yosys synthesizer, which treats a latch inside always_comb as an error. Commercial tools usually report it as a warning (for example “latch inferred”).
Plain always @(*) carries no such intent. Yosys still logs that it inferred a latch, but it carries on and the synthesis succeeds; whether a commercial tool treats the same code as a warning, an error or an info message depends on the tool and its configuration. Success is not a clean report. always_comb adds an intent check that turns the latch into a diagnostic you can act on, which is the reason to prefer it.
The bottom row, y, holds its old value while en is low.
Slot 1: before any input moves, y is unknown (x).
Slot 2: en rises with a at 1, so y becomes 1.
Slot 3: en drops to 0 while a changes to 0, but y holds 1.
Slots 4-5: a toggles twice while en stays 0, and y still holds 1.
Slots 6-7: en rises again with a at 0, so y follows down to 0.
That held 1 is the symptom: simulation passes, but the netlist holds a memory cell.
Missing select value is the same bug in a mux. Select 3 has no branch, so the output must hold.
// yosys: latch
module latch_case (input logic [1:0] sel, input logic a, b, c, output logic y);
always_comb begin
case (sel)
2'd0: y = a;
2'd1: y = b;
2'd2: y = c;
// sel == 3 is missing and there is no default -> y holds its value
endcase
end
endmodulePartly assigned output hides inside a healthy block. Only y grows a latch here.
// yosys: latch
module latch_partial (input logic sel, a, b, output logic x, y);
always_comb begin
x = a; // x is always assigned
if (sel) y = b; // y is not: latch on y only
end
endmoduleThe third case is the sneakiest: the block looks assigned because x is, and a quick read misses that y is not.
Conditional assign inside a loop is the same gap in disguise. Zero set bits means zero assignments.
// yosys: latch
module latch_loop (input logic [3:0] sel, input logic [7:0] d, output logic [7:0] y);
always_comb begin
for (int i = 0; i < 4; i++)
if (sel[i]) y = d; // if sel == 0, nothing assigns y, so y holds its old value
end
endmoduleThe loop always runs 4 times, but the assignment inside it is conditional. When sel is zero, no pass assigns y.
So y must keep its old value, and that keeping needs a latch. Fix it by giving y a default value before the loop.
Cause | Symptom | Fix |
|---|---|---|
| Output holds when the condition is false | Add |
| Output holds for uncovered select values | Add |
Output assigned on some branches only | That one output holds | Assign every output on every path |
Assignment inside a loop that may not run | Output holds if the loop never assigns | Initialize before the loop |
always_comb block assigns y only when en is 1. Now en is 0. What does y hold, and what does synthesis build?It holds its old value. The missing path must remember, so synthesis builds a latch.
y only when some bit of sel is set. sel is zero. What does y hold?Its old value. No pass assigns it, so the value must be held, and holding needs a latch.
case over a 2-bit select that lists values 0, 1 and 2 only, why is a latch inferred even though “the select can never be 3”?The tool cannot know that. It must produce correct behaviour for every possible select value, including 3, and with no assignment for 3 the output has to hold. Cover 3 with a default (or assign a default before the case): that is the complete-assignment fix. A separate assertion can flag a select value that should never occur. unique case is a different promise: it says 3 never happens, so synthesis may treat it as a don't-care, and it belongs only with an explicit assumption that you also check.
A likely interview question is why an if without else infers a latch.
Missing path leaves the output unassigned for some input.
Holding the old value needs storage, so synthesis builds a latch.
Avoid it with a default assignment first, and write always_comb so the tool reports it.
One testbench shows the symptom: the output keeps its old value.
module tb_latch_symptom;
timeunit 1ns; timeprecision 1ps;
logic en = 0, a = 0, y;
always @* if (en) y = a; // the buggy pattern (always @* so the simulator accepts it)
int errors = 0;
initial begin
#1 en = 1; a = 1; #1;
if (y !== 1'b1) begin // follows a while enabled
$display("ERROR: en=1 a=1 but y=%b", y);
errors++;
end
en = 0; #1;
a = 0; #1; // en is low: y should ignore a...
if (y !== 1'b1) begin // ...so it HOLDS 1 (storage!), not 0
$display("ERROR: en=0 and y=%b, expected the held 1", y);
errors++;
end
if (errors != 0) $fatal(1, "FAIL: tb_latch_symptom errors=%0d", errors);
$display("PASS: the output remembers its last value, which is the symptom of a latch");
$finish;
end
endmodule