The always_comb latch error, explained
Debugging a specific message? You’re in the right place. For the underlying concept, see inferred latches in Verilog or in VHDL.
If synthesis just stopped with something like:
ERROR: Latch inferred for signal `\grant.\grant_out' from always_comb process
or, in friendlier tools:
Latch inferred for 'grant_out': always_comb requires every output to be
assigned in every branch. Add an else or a default assignment at the top
of the block (or use always_latch if a latch is intended).
then your always_comb block has a path through it that doesn’t assign one of
its outputs, and the tool is refusing to do what plain Verilog would have
done silently: build a level-sensitive latch to remember the old value.
This error is a feature. always_comb isn’t just a shorter spelling of
always @(*). It’s a promise to the tools that the block describes purely
combinational logic. When the code breaks that promise, the tool errors
instead of quietly inserting storage you didn’t ask for.
Watch the difference
This block uses old-style always @(*), so the missing else silently
becomes a latch; you can see it in the schematic. Now change always @(*) to
always_comb and press Run: the same code becomes a hard error, with the
message pointing at the block:
Interactive schematic by RapidRTL
Same hardware question, two philosophies: @(*) assumes you meant it;
always_comb makes you say what you meant.
The fixes
The error names a signal; make sure that signal is assigned on every path through the block. Either:
always_comb begin
grant_out = 1'b0; // default at the top: every path now assigns
if (enable)
grant_out = req;
end
or complete the branch structure (else, default: in every case). The
full menu of idioms is in the
inferred-latches guide.
If the latch is intentional, say that too. That’s what always_latch
is for:
always_latch begin
if (enable)
q <= d; // deliberately a latch; tools now expect one
end
The intent family, in one table
| Block | Promise to the tools | Broken promise becomes |
|---|---|---|
always_comb | purely combinational | hard error (this page) |
always_ff @(posedge clk) | clocked registers only | error if no storage is implied |
always_latch | level-sensitive latch | error if no latch is inferred |
always @(*) | none (legacy Verilog-2001) | silent latch, maybe a warning |
If your codebase still uses always @(*), the cheapest lint rule you will
ever adopt is replacing it with always_comb: this whole class of bug turns
from a warning someone ignores into an error nobody can.
Why did it work in simulation?
Simulators happily model the latch: the block simply keeps the old value, and
your testbench may never toggle the inputs in the order that exposes it.
That’s the trap. The code looks verified, and synthesis (or always_comb)
is where the promise gets checked against the hardware.