Skip to main content

RTL Linting

Every technique covered so far — CDV, formal, CDC, RDC — needs the design to actually simulate or run through a formal engine first. Linting doesn't: it's static analysis of the RTL's own structure, catching a real and common class of bug with zero stimulus, zero testbench, and zero simulation time — the cheapest, earliest gate in a real front-end flow.

Combinational loops​

assign y = a ? b : y; // y depends on itself with no clocked element in between

A feedback path with no flip-flop anywhere in the loop has no defined settling behavior at all — in simulation, it's a race condition evaluated at zero simulation time, with the output oscillating unpredictably between values rather than converging; in real silicon, the equivalent circuit has no guaranteed stable state either. Lint catches this structurally, by tracing the signal dependency graph for a cycle with no register in it, without needing to simulate a single vector that would expose the resulting X or oscillation.

Multi-driven nets​

assign z = sel ? a : 1'bz;
assign z = 1'b1; // z now has two unconditional drivers at once

A net driven by more than one source at the same simulation time has no single defined value — most simulators resolve it to an indeterminate X, which is itself just a symptom masking whichever real bug caused two drivers to exist in the first place (a missing else, a copy-paste duplicate assignment, two blocks that were each supposed to own a different condition). Lint flags the structural conflict directly, rather than waiting for it to surface as a mysterious X several simulation stages later.

Latch inference​

always_comb begin
if (enable)
q = d;
// no else — what should q be when enable is 0?
end

When a combinational block doesn't assign a variable on every possible path through it, synthesis has exactly one option: infer a latch to hold the variable's previous value on the unassigned path, since combinational logic can't have "no answer." This is almost never what the RTL author intended — latches complicate timing closure, behave less predictably under back-annotated timing, and often silently indicate an incomplete if/case rather than a deliberate design choice. Lint catches an incomplete assignment path the moment it's written, long before it becomes an unexpected latch in a synthesis report.

Unconnected ports​

An instance's input left unconnected, or an output nobody reads, is either genuinely dead (worth removing, since it's easy to mistake for something being used) or a real wiring mistake (a signal that was supposed to connect and doesn't) — and it's often impossible to tell which just from the code around it. Lint flags both cases explicitly rather than letting a floating input silently default to whatever value happens to result, which varies by simulator and tool and is exactly the kind of ambiguity a real design can't afford.

An unread output specifically has a second, synthesis-side consequence beyond simulation ambiguity: synthesis will typically optimize away the entire combinational logic cone that drives it, since nothing downstream ever consumes the result. If that output was actually supposed to be used somewhere and simply wasn't wired up, the missing connection doesn't just leave a signal floating — it silently deletes real logic from the synthesized netlist, with no error, because from the tool's perspective unread logic is legitimately dead logic.

Incomplete sensitivity lists​

always @(a or b) // c is read inside but missing from the sensitivity list
y = a & b & c;

A combinational always block whose sensitivity list doesn't include every signal it reads creates a real, specific hazard: simulation only re-evaluates the block when something on the listed sensitivity triggers it, so a change to c alone silently doesn't update y in simulation — but synthesis, which always builds combinational logic sensitive to everything the block reads regardless of what the list says, produces a gate-level circuit that does react to c. The RTL and the netlist it synthesizes to disagree on behavior, purely from an incomplete list — one of the clearest, most classic cases of simulation-versus-synthesis mismatch. Lint (or SystemVerilog's always_comb/always @*, which build the sensitivity list automatically and eliminate the mistake by construction) catches this before it ever produces a confusing "works in RTL sim, wrong on gates" bug report.

Why this gate runs first​

Every one of these four issues is catchable with no stimulus at all — the RTL's own structure is the only input a lint tool needs. Running lint before CDC/RDC analysis, before GLS, before the first line of a testbench is written, catches a real, common class of bug at the cheapest possible point on the cost-of-bugs curve — before any of the more expensive techniques in this section even have something meaningful to analyze.

What's next​

Lint operates purely on RTL structure. The next page moves to the opposite end of the front-end flow — simulating the design after synthesis, against the actual gates and timing that will be fabricated, and a specific, well-documented class of bug that only shows up there.