Visual walkthrough — Combinational logic in HDL
We assume nothing. If you have never seen a truth table, a gate, or a line of HDL, start at line one — everything is built here.
Parent: Combinational logic in HDL.
Step 1 — What a "combinational rule" really is
WHAT. Imagine a light with one switch called and one wire of data called . We want to build a box that produces an output . A combinational box is a lookup table: for every possible input, it lists exactly one output. Nothing is left blank.
Here is a single wire that is either (off) or (on). is likewise or . So there are only four possible input situations. A complete combinational rule must fill in all four boxes.
WHY. A rule with a blank box is not a function — it does not say what to do. And "what to do when I didn't say" is the exact crack through which memory leaks in. So before any code, we look at the table and ask: are all boxes filled?
PICTURE. The full 4-row table. The accent (red) column is — the thing we must define in every row.

Step 2 — Writing the tempting (broken) code
WHAT. A beginner writes exactly what they care about:
always @(*) begin
if (enable)
y = data; // only says what happens when enable == 1
endLet us read this like a sentence. always @(*) means "re-run this whole block whenever any input changes." The if (enable) means "when is , do the next line." y = data copies onto .
WHY. It feels efficient: "I only need when is on, so why write the off case?" This intuition is the trap — it is why the bug feels correct.
PICTURE. The code drawn as a decision fork. The red branch (the if body) is the only path that assigns . The other branch is empty — no assignment.

Step 3 — Overlaying the code onto the table
WHAT. We now colour the truth-table rows by which code path handles them. The two rows where are covered by path A (red — assigned). The two rows where are covered by path B — but path B does nothing, so those output cells stay blank.
WHY. This is the moment of proof. Step 1 said a combinational box needs all four boxes filled. Step 2's code only fills two. So the code does not describe the complete table — half of it is missing.
PICTURE. The table again, but now the two output cells are shown as red empty boxes: "undefined by the code."

Step 4 — What the tool must do with a hole
WHAT. The synthesizer (the program that turns your code into gates) reads the code and asks: "When , what should be?" Your code never answers. But real hardware wires cannot be "undefined" — a wire must carry a value. So the tool needs a rule for the silent case. The only rule consistent with "you didn't tell me to change it" is: keep whatever was before.
WHY. "Keep the previous value" is memory. To keep a value, the hardware must store it — and a storage element that holds a value while and passes new data while has a name: a transparent latch. The tool is not being clever or dumb; it is doing the only thing that satisfies your incomplete description.
PICTURE. A single arrow diagram: your code (with the hole) → the tool's forced question → the latch it must build. The latch symbol is the red key object.

Step 5 — Watching the latch remember (a timing walk)
WHAT. Let us feed the buggy circuit real signals over time and watch . Suppose:
- → follows → .
- drops to (while later changes to ) → stays , ignoring the new .
- rises to again → follows once more.
WHY. This is the observable symptom of the bug: holds a stale value through the whole window. That "stuck at the old value" plateau is the latch remembering. A pure combinational would have no dependence on the past at all — but here it clearly does.
PICTURE. Three stacked waveforms — , , and (red) . The red plateau where ignores is the memory made visible.

Step 6 — The fix: fill every box with a default
WHAT. We add one line before the if so that no path can be silent:
always @(*) begin
y = 1'b0; // DEFAULT: fills the enable==0 rows
if (enable)
y = data; // overrides in the enable==1 case
endRead the flow: first is set to unconditionally. Then — only if — it is overwritten with . Because the block uses blocking = (executes in order, top to bottom, like signals rippling through gates), the second line simply replaces the first when it runs.
WHY. Now trace both paths: if , becomes then → defined. If , the if body is skipped, so keeps its default → also defined. Every row of the Step 1 table is now filled. No hole ⇒ no forced memory ⇒ no latch. The tool builds a plain multiplexer instead.
PICTURE. The same table as Step 3, but now the two formerly-empty cells are filled with a red from the default — the hole is closed.

Step 7 — The edge cases (so no scenario surprises you)
WHAT. We check the corners a careless reader might hit.
- Multi-bit outputs. If is a bus (many wires), the default must cover every bit, e.g.
y = 4'b0000;. A default that sets only some bits leaves the rest with holes → per-bit latches. casewithoutdefault. Acase (sel)that lists some values but not all is the same hole in disguise — unlisted values leave silent → latch. Fix: adddefault:(Verilog) orwhen others =>(VHDL). See Multiplexers, adders, decoders.- The "always driven" idiom. A continuous
assign y = enable ? data : 1'b0;has no branch that can be skipped — the?:names both outcomes. This is whyassignis inherently safe (parent, Idiom 1). - Genuinely wanting memory. If you do want to hold — that is Sequential logic in HDL, built on purpose with a clock and non-blocking
<=. The latch is only a bug when it appears in code that was meant to be combinational.
WHY. The latch trap is not one line of code — it is the general shape "a path that leaves an output unassigned." Every case above is that same shape. Recognising the shape is the real skill.
PICTURE. A "hole detector" flow: does some input combination leave the output unassigned? Yes → latch (red). No → combinational.

Recall Reveal — spot the latch
Does always @(*) begin case(sel) 2'b00: y=a; 2'b01: y=b; endcase end infer a latch? ::: Yes — and are unlisted, so is unassigned there. Add a default:.
The one-picture summary
Everything on this page compressed into a single diagram: an unassigned path forces a hold, a hold is a latch, and a default closes the path.

Recall Feynman: retell the whole walkthrough
Think of the box as a form with one row for every button combination. Step 1: a good combinational box fills in every row. Step 2–3: the tempting code only writes the " on" rows and leaves the " off" rows blank. Step 4: a real wire can't be blank, so the tool asks "what now?" and, getting no answer, decides "just keep the old value" — and to keep a value you need a tiny memory cell, the latch. Step 5: you can literally see it: wiggle while is off and sits frozen, remembering. Step 6: the cure is one line — write a default value first, so even the "off" rows are filled; now nothing is blank, the tool builds a clean selector, and the memory vanishes. Step 7: the same "blank row = latch" trap hides in multi-bit outputs and in case without default — spot the blank, fill the blank. That's the entire art: never leave a row empty.
Connections
- Latch inference and how to avoid it — the rules this page derived from pictures.
- Blocking vs Non-blocking assignments — why the default-then-override trick relies on blocking
=. - Sequential logic in HDL — where holding a value is the goal, not a bug.
- Multiplexers, adders, decoders — the clean circuit the tool builds once the holes are gone.
- Boolean algebra and Karnaugh maps — the truth tables our steps kept filling in.
- Synthesis and the digital design flow — the tool that turns these tables into gates.