A latch holds the old value while its enable is low, and passes the input through while the enable is high. A synthesis tool infers one 8-bit $dlatch (8 latch bits after mapping) whenever some path through a combinational block skips an output, because holding the old value is the only hardware that reproduces the simulation behaviour of leaving a variable untouched. The rule covers every shape of gap: a missing else, a case without a default, a default assigned to only some of several outputs. Width does not matter to the rule, only coverage does: an 8-bit bus skipped on one path needs eight held bits, which the tool packs into one $dlatch cell carrying all eight.
This lesson runs five variants of one 8-bit gated buffer through proc and records the $dlatch count of each: the bare if without an else, three classic fixes, and an always_comb version with complete assignment. The table below is the answer key, read from the recorded netlists rather than asserted by hand. Predict first, then check yourself against it.
Only the bare variant infers storage: it records 1 $dlatch cell with no select cells, while the default-first, added-else, case-default and always_comb variants record 0, 0, 0 and 0 latches respectively. Every fix closes the skipped path, so the hold requirement vanishes.
The script reads each variant on its own with read_verilog -sv, which is the Yosys front end that also accepts SystemVerilog keywords such as always_comb. Each phase selects its top, runs proc, and records stat plus the JSON netlist. The bare variant asserts exactly one $dlatch with no flop; every fixed variant asserts no $dlatch with at least one select cell. One PASS line covers all five phases, so a passing run certifies the before and every after in a single stroke.
read_verilog -sv <<EOV
module latch_bad (
input wire en,
input wire [7:0] d,
output reg [7:0] q
);
always @* begin
if (en)
q = d;
end
endmodule
EOV
hierarchy -check -top latch_bad
proc
tee -o stat_bad.txt stat
write_json net_bad.json
select -assert-count 1 t:$dlatch
select -assert-none t:$dff
design -reset
read_verilog -sv <<EOV
module fix_default (
input wire en,
input wire [7:0] d,
output reg [7:0] q
);
always @* begin
q = 8'd0;
if (en)
q = d;
end
endmodule
EOV
hierarchy -check -top fix_default
proc
tee -o stat_fix_default.txt stat
write_json net_fix_default.json
select -assert-none t:$dlatch
select -assert-min 1 t:$mux t:$pmux %u
design -reset
read_verilog -sv <<EOV
module fix_else (
input wire en,
input wire [7:0] d,
output reg [7:0] q
);
always @* begin
if (en)
q = d;
else
q = 8'd0;
end
endmodule
EOV
hierarchy -check -top fix_else
proc
tee -o stat_fix_else.txt stat
write_json net_fix_else.json
select -assert-none t:$dlatch
select -assert-min 1 t:$mux t:$pmux %u
design -reset
read_verilog -sv <<EOV
module fix_case (
input wire en,
input wire [7:0] d,
output reg [7:0] q
);
always @* begin
case (en)
1'b1: q = d;
default: q = 8'd0;
endcase
end
endmodule
EOV
hierarchy -check -top fix_case
proc
tee -o stat_fix_case.txt stat
write_json net_fix_case.json
select -assert-none t:$dlatch
select -assert-min 1 t:$mux t:$pmux %u
design -reset
read_verilog -sv <<EOV
module fix_sv (
input wire en,
input wire [7:0] d,
output reg [7:0] q
);
always_comb begin
q = 8'd0;
if (en)
q = d;
end
endmodule
EOV
hierarchy -check -top fix_sv
proc
tee -o stat_sv.txt stat
write_json net_sv.json
select -assert-none t:$dlatch
select -assert-min 1 t:$mux t:$pmux %u
log PASS: bare branch infers one latch and every fix removes itVariant | $dlatch cells (recorded) | Select cells (recorded) | Verdict |
|---|---|---|---|
Bare if, no else | 1 | 0 | Latch: enable low must hold q. |
Default assigned first | 0 | 1 | Mux: every path assigns q. |
Missing else added | 0 | 1 | Mux: every path assigns q. |
Case with default | 0 | 1 | Mux: every value assigns q. |
always_comb, complete | 0 | 1 | Mux: SystemVerilog, same rule. |
Read the first row against the other four. The bare variant is the only one with a skipped path, and it is the only one with a latch; the four fixes differ in syntax but agree in coverage, and the fixed variants agree in hardware. That agreement is the point of the table: the tool does not grade your style, it checks your coverage, and every complete coverage compiles to the same select shape.
SystemVerilog offers always_comb for exactly this intent: combinational logic with no storage. The fix_sv variant assigns a default first and completes the branch inside always_comb, and the recorded run treats it like its always @* twin, with no latch and the same select shape. Two details matter for everyday use. First, Yosys reads it only with the SystemVerilog front end, hence read_verilog -sv in the script; the plain Verilog reader would reject the keyword. Second, several simulators and lint flows warn when an always_comb block can hold a value, which turns the latch from a silent netlist surprise into a compile-time message. Prefer always_comb for new combinational blocks, and read any warning on it as the tool pointing at the exact path you skipped.
module latch_sv (
input wire en,
input wire [7:0] d,
output logic [7:0] q
);
always_comb begin
q = 8'd0;
if (en)
q = d;
end
endmoduleTake the bare variant and change only its block header to always_comb, keeping the missing else. Run the listing below by hand: Yosys stops inside proc with the error "Latch inferred for signal latch_acomb.q from always_comb process", naming the signal and the block (hand-observed message, exact cell prefix aside). The course runner treats any ERROR line as a failed block and allows no logger override, so this refusal cannot ship as a recorded block; the listing stays hand-run. SystemVerilog reserves always_comb for gap-free combinational logic, so the tool refuses to build the memory instead of inferring it silently. That refusal is the feature: the construct that hides the latch in always @* becomes a compile error here, pointing at the exact path you skipped. Deleting any fixed variant's safety line and re-elaborating reproduces the bare row the other way: the symptom is a signal that keeps its old value after its inputs changed, and the fix is the missing assignment, never a constraint.
# not-run: Yosys stops this variant with an ERROR line, which fails a recorded block and allows no logger override, so run this listing by hand
read_verilog -sv <<EOV
module latch_acomb (
input wire en,
input wire [7:0] d,
output reg [7:0] q
);
always_comb begin
if (en)
q = d;
end
endmodule
EOV
hierarchy -check -top latch_acomb
proc
log PASS: always_comb with a skipped path is rejectedOne 8-bit $dlatch (8 latch bits after mapping): the recorded bare netlist shows 1 latch cell. Each bit holds its old value on the skipped path, packed into a single recorded cell.
Every path now assigns the output, so the latch count falls from 1 to 0 while 1 select cell carries the choice. The memory has nothing left to remember.
Every count on this page comes from one Yosys 0.70+post run recorded by the course checker: read_verilog -sv reads the latch plus three fixed variants plus an always_comb version, each runs proc, and stat plus write_json are recorded per variant. The single figure is the recorded bare-variant netlist, which holds one 8-bit $dlatch (8 latch bits after mapping) and no select cells; the hold behaviour is described from that figure, not from a waveform. The always_comb refusal stays a hand-run listing with its observed message quoted, because the runner fails any block that prints an ERROR line and allows no logger override.