Interview Traps: HDL & Verilog Semantics

Correctly distinguish simulation from synthesis, explain why nonblocking assignment and complete case/if coverage matter in RTL, and separate Verilog's bitwise (&, |) from logical (&&, ||) operators — the HDL-specific traps that catch anyone who treats Verilog like an ordinary software language.

This is the cluster where 'I know Verilog syntax' and 'I understand what Verilog describes' visibly diverge. Every question here has bitten a real engineer at some point — code that simulates perfectly but synthesizes into broken or different hardware, an accidentally inferred latch, a bitwise operator silently producing the wrong width. Interviewers ask these because they are exactly the bugs code review and lint tools exist to catch, and a candidate who can explain WHY the bug happens (not just recite the rule) is the candidate who will catch it before it reaches silicon.

What is the difference between simulation and synthesis?

Simulation runs HDL code on an event-driven simulator against a testbench to verify functional and timing behavior, entirely in software — nothing physical is produced. Simulation can freely use behavioral, non-hardware-representable constructs: explicit time delays (#10), console output ($display), initial blocks, and general-purpose looping/variable constructs that have no direct correspondence to any physical circuit, because the simulator is just executing a model, not building hardware.

Synthesis is a completely different process: a synthesis tool reads a restricted, 'synthesizable' subset of the HDL and translates it into an actual gate-level (or LUT-level, for FPGAs) netlist — real flip-flops, real combinational logic, mapped onto a specific target technology. Constructs with no hardware meaning (delays, $display, unbounded loops) are either ignored, flagged as errors, or silently produce something other than what the designer intended.

The trap answer treats them as basically the same thing, just 'simulation runs first, then synthesis builds what was verified' — this misses that a design can simulate correctly while relying on constructs that either fail to synthesize at all, or synthesize into DIFFERENT hardware than what the simulation modeled (the classic case being sequential logic written with blocking assignments, discussed next), meaning a passing testbench is not proof that the synthesized chip will behave the same way.

Why is <= commonly used in always_ff?

<= is the nonblocking assignment operator. Inside a clocked block (always_ff, or always @(posedge clk)), every nonblocking assignment's right-hand side is evaluated using the values signals held just before the clock edge, and ALL of the left-hand-side updates are scheduled to take effect together, after every statement in every always block has been evaluated for that time step — modeling the way real flip-flops actually work: on a clock edge, every flip-flop samples its D input simultaneously and updates simultaneously, with no ordering dependency between different flip-flops.

Using blocking assignment (=) in sequential logic breaks this model, because blocking assignments update their left-hand side immediately, in the textual order they execute — so a later statement in the same or another always block can see the NEW value of a signal that was 'just updated' rather than the value that existed before the clock edge, which does not match how real hardware behaves and makes simulation results depend on statement/block execution order rather than on the actual hardware structure.

The trap answer is 'they do the same thing, it's just a style choice.' The concrete failure case is a two-flip-flop shift/swap: with a <= b; b <= a; inside an always_ff block, both flip-flops correctly swap values on the clock edge, exactly like real hardware, because both right-hand sides use pre-edge values. Written with blocking assignment as a = b; b = a;, the second line uses the just-updated value of a (which now equals the old b) — so b ends up equal to the old b instead of the old a, and the simulated result no longer matches what a real pair of flip-flops does. Nonblocking assignment isn't a style preference; it's what makes the simulation model match real concurrent hardware.

Why can incomplete always_comb assignments infer a latch?

Combinational logic, by definition, must have its output determined completely by its current inputs at all times — for every possible combination of inputs, some value must be assigned to every output. If an always_comb (or always @(*)) block has a path through its if/else or case logic where a signal is NOT assigned on that path (a missing else branch, or a case statement without a default that doesn't cover every possible input value), the synthesis tool is left with an impossible requirement: what should that output be when the unlisted input condition occurs?

The only hardware that can satisfy 'output stays whatever it was previously when this condition holds, and updates when this other condition holds' is a level-sensitive latch — a storage element. So the synthesis tool infers one, automatically, to make the description implementable: it adds a real latch to hold the last-assigned value whenever the missing branch's condition is true. This is virtually always unintended by the designer, since the block was written with the intent of being purely combinational.

The trap answer downplays this as merely a simulation quirk — 'the simulator just remembers the last value, it's not a real hardware effect.' That is wrong and dangerous to believe: synthesis tools infer an actual physical latch in the resulting circuit, not just a simulation artifact, and unintended latches are one of the most common and most serious RTL bugs, because they introduce real sequential (timing-sensitive, potentially glitch-prone) behavior into what was supposed to be simple, predictable combinational logic — which is exactly why every professional linting flow flags incomplete case/if coverage in combinational blocks as an error.

Difference between & / && and | / || in Verilog?

& and | are bitwise operators: given two operands (typically multi-bit vectors), they perform the AND or OR operation independently on each corresponding bit position, and the result has the same width as the operands — this is the same 'combine two buses bit by bit' operation you would draw as an array of individual AND or OR gates.

&& and || are logical operators: they treat each ENTIRE operand as a single boolean value first (any nonzero value counts as true, exactly zero counts as false), then apply logical AND/OR to those two booleans, producing a single 1-bit result (0 or 1) — this is the operator you want in conditional expressions like if statements, where you're asking 'are both of these conditions true?' rather than 'combine these buses bit by bit.'

The trap is treating them as interchangeable because both 'mean AND' or 'mean OR' in casual reading. Concretely, for 4-bit vectors 4'b0110 and 4'b1001: the bitwise result 4'b0110 & 4'b1001 = 4'b0000 (no bit positions match), while the logical result 4'b0110 && 4'b1001 = 1'b1, because both operands are nonzero (true) even though they share no set bits in common — a completely different answer from the same two operands. A closely related trap is using | inside an if-condition intending logical OR-of-conditions: with single-bit operands the two behave identically, which lets the bug hide until someone uses a multi-bit expression, at which point | produces a bit-vector rather than the intended single boolean, and depending on context that can silently evaluate 'true' for the wrong reason or produce a width-mismatch warning that's easy to ignore.

Why are HDL constructs not equivalent to ordinary software statements?

HDL code describes a piece of hardware structure that exists all at once and operates concurrently, not a sequence of instructions that execute one after another on a processor. Multiple always blocks in a Verilog module all run 'simultaneously' — they represent physically separate, always-active pieces of hardware, not competing threads that get scheduled — and a for loop inside a block destined for synthesis does not iterate at runtime the way a software loop does; if the loop has a fixed, compile-time-known bound, the synthesis tool 'unrolls' it into that many copies of replicated parallel hardware, all existing simultaneously in the final circuit, rather than one piece of hardware executing repeatedly over time.

Similarly, an assignment in HDL describes a wire or register that continuously holds or continuously reflects a value — it isn't 'executed and then finished,' the way a software assignment statement runs once and the program counter moves on. And the blocking-vs-nonblocking distinction covered above has no real counterpart in typical imperative software languages at all, because software has no concept of 'many things updating in parallel on the same triggering event.'

The trap — really the root cause behind most of the other traps in this lesson — is treating Verilog as 'a programming language like C, so normal programming instincts apply.' That assumption is exactly what leads to accidentally inferring a latch (thinking 'the last value will just be remembered, like a variable' rather than seeing that a real storage element must be built), misusing blocking assignment in sequential logic (thinking of assignments as executing 'in order' the way software statements do), or writing a loop and expecting it to iterate at runtime rather than being unrolled into parallel hardware at synthesis time. The single most important mental shift for correct HDL design is to read every construct as 'a description of hardware structure that exists,' not 'a set of instructions that run.'

Bitwise vs. Logical Operators, and a Complete case Statement

A default branch (or an explicit else on every if) guarantees every input combination assigns every output, which is what prevents latch inference. The same latch-inference rule applies to if/else chains: an if with no matching else is just as dangerous as a case with no default.

// Bitwise vs logical, 4-bit example
wire [3:0] a = 4'b0110;
wire [3:0] b = 4'b1001;
wire [3:0] bitwise_and = a & b;   // 4'b0000 (per-bit AND)
wire       logical_and = a && b; // 1'b1    (a!=0 AND b!=0)

// Incomplete case -> latch inferred (BUG)
always_comb begin
  case (sel)
    2'b00: y = in0;
    2'b01: y = in1;
    // 2'b10 and 2'b11 not covered, no default -> y must
    // 'hold' its previous value here -> latch inferred
  endcase
end

// Fixed: every path assigns y -> purely combinational
always_comb begin
  case (sel)
    2'b00: y = in0;
    2'b01: y = in1;
    default: y = in0; // or a defined default value
  endcase
end

Code Examples

Nonblocking assignment evaluates every right-hand side using pre-clock-edge values and applies all updates together, exactly matching how two real flip-flops driven by the same clock behave -- neither one can 'see' the other's new value until the next edge. Blocking assignment executes in textual order within the simulation time step, so the second statement sees the first statement's already-updated value, which does not correspond to any real hardware and produces a functionally wrong swap.

<= evaluates all RHS values before any LHS updates; = updates immediately, breaking the parallel-hardware model.

// Correct: nonblocking, models real simultaneous flip-flop update
always_ff @(posedge clk) begin
  a <= b;   // RHS uses pre-edge value of b
  b <= a;   // RHS uses pre-edge value of a (NOT the new a above)
end
// Result: a and b correctly SWAP every clock edge.

// Buggy: blocking, executes top-to-bottom like software
always_ff @(posedge clk) begin
  a = b;    // a immediately takes b's value
  b = a;    // BUG: reads the just-updated a, which now equals
            // the OLD b -- so b is set to its own old value,
            // not to the old a. No swap actually happens.
end

Why these are the bugs RTL code review exists to catch

Unintended latch inference and blocking-assignment-in-sequential-logic are two of the most common bugs caught by professional RTL linting tools (and by code review) precisely because they pass simulation silently and only reveal themselves after synthesis — or worse, in the fabricated chip. A design team that verifies only in simulation without also running synthesis and static lint checks can ship a chip whose gate-level behavior does not match its simulated behavior. The bitwise-vs-logical operator distinction (& vs && , | vs ||) matters because a single mistaken operator can silently change a condition's result width or logic from what was intended, and because it works 'by accident' for single-bit signals, the bug often only appears when the code is later reused with a multi-bit signal. Understanding that HDL describes concurrent hardware structure rather than sequential instructions is the conceptual foundation every other RTL best practice (complete case coverage, nonblocking assignment in sequential blocks, avoiding runtime-variable loop bounds) is built on.

A case statement without a default is fine as long as the simulation testbench happens to never exercise the missing cases -- the simulator will just keep the old value, which is harmless.

An incomplete case (or if without a matching else) inside combinational logic causes the synthesis tool to infer a REAL physical latch, regardless of whether the testbench ever exercises the missing input combination -- this is a hardware bug present in the synthesized chip, not merely a simulation behavior that can be tested around.

This mistake assumes the danger is only about what the testbench happens to exercise. In reality, the synthesis tool must build hardware that satisfies the description for EVERY possible input, including ones the testbench never tests, so an untested missing branch still becomes a latch in the real circuit -- which is exactly why lint tools flag incomplete case/if coverage as an error independent of simulation coverage.

Why can code that simulates correctly still fail to match the hardware produced by synthesis? A. Simulation and synthesis always produce identical results by definition, so this cannot happen. B. Simulation executes a behavioral software model that may use constructs (delays, unrestricted loops, blocking assignment misuse in sequential logic) with no direct hardware equivalent, while synthesis maps only a restricted, synthesizable subset of the code onto real gates/flip-flops — so a passing testbench does not guarantee the synthesized netlist behaves the same way. C. Synthesis tools always ignore always_ff blocks entirely. D. Simulation only checks syntax, never behavior.

B. Simulation executes a behavioral software model that may use constructs (delays, unrestricted loops, blocking assignment misuse in sequential logic) with no direct hardware equivalent, while synthesis maps only a restricted, synthesizable subset of the code onto real gates/flip-flops — so a passing testbench does not guarantee the synthesized netlist behaves the same way. — Simulation verifies a behavioral model; synthesis translates a restricted subset of that same code into real hardware. Constructs outside the synthesizable subset, or coding styles (like blocking assignment in sequential logic) that simulate one way but imply different real hardware behavior, are exactly why passing simulation is not proof the synthesized chip will behave identically.

Why is nonblocking assignment (<=) preferred over blocking assignment (=) inside an always_ff clocked block? A. It is purely a stylistic convention with no functional difference. B. Nonblocking assignment evaluates every right-hand side using pre-clock-edge values and applies all left-hand-side updates together, correctly modeling how real flip-flops simultaneously sample and update on a clock edge; blocking assignment updates immediately in textual order, which can produce results (e.g., in a register swap) that don't match real hardware. C. Nonblocking assignment runs faster in simulation. D. Blocking assignment cannot be used inside any always block.

B. Nonblocking assignment evaluates every right-hand side using pre-clock-edge values and applies all left-hand-side updates together, correctly modeling how real flip-flops simultaneously sample and update on a clock edge; blocking assignment updates immediately in textual order, which can produce results (e.g., in a register swap) that don't match real hardware. — Real flip-flops driven by the same clock all sample their inputs at the same instant and update together, with no ordering dependency. Nonblocking assignment matches this by using pre-edge values for every right-hand side; blocking assignment's immediate, textual-order updates can let a later statement see an already-updated value, breaking the parallel-hardware model, as the classic two-register swap example shows.

Why does an incomplete case statement (missing a default, not covering every input value) inside an always_comb block infer a latch? A. It doesn't actually cause any hardware difference, only a simulation warning. B. Because the output must hold its previous value whenever the unlisted input condition occurs, and the only hardware element that can 'remember and hold a previous value until a condition changes' is a level-sensitive latch, which the synthesis tool inserts to satisfy that requirement. C. Because Verilog automatically converts all case statements into latches for speed. D. Because latches are required by the language whenever a case statement is used at all.

B. Because the output must hold its previous value whenever the unlisted input condition occurs, and the only hardware element that can 'remember and hold a previous value until a condition changes' is a level-sensitive latch, which the synthesis tool inserts to satisfy that requirement. — Combinational logic must define an output for every input combination. When a branch is missing, the only way to give the output SOME value for that condition is to hold whatever it last was — exactly the behavior of a latch — so synthesis infers a real physical latch, not just a simulation artifact.

For 4-bit signals a = 4'b0110 and b = 4'b1001, what do a & b and a && b evaluate to, respectively? A. a & b = 4'b1111 and a && b = 4'b1111 B. a & b = 4'b0000 (bitwise AND, no matching bit positions) and a && b = 1'b1 (both operands are nonzero, so logical AND of two true values is true) C. a & b and a && b always produce the same result for any operands D. a & b = 1'b1 and a && b = 4'b0000

B. a & b = 4'b0000 (bitwise AND, no matching bit positions) and a && b = 1'b1 (both operands are nonzero, so logical AND of two true values is true) — & is bitwise: it ANDs each bit position independently, and 0110 shares no set bit positions with 1001, giving 0000. && is logical: it reduces each operand to a single boolean (nonzero = true) first, and since both a and b are nonzero, true && true = 1. The two operators can give very different results on the same operands.

Rewrite the following always_comb block to eliminate its inferred latch, and explain in one sentence which specific missing coverage caused the inference: always_comb begin if (mode) y = x + 1; end always_comb begin if (mode) y = x + 1; else y = x; end (or any defined else-branch value). The original code left y unassigned whenever mode was false, so the synthesis tool had to infer a latch to hold y's previous value for that condition; adding an else branch that assigns y for every possible value of mode makes the block purely combinational.

Every input combination must assign y something. What happens to y when mode is false?