RTL Verification

Eliminating False Positives in RTL Linting

8 min read Elena Rodriguez
Abstract visualization of signal noise filtering in RTL analysis

The most common complaint we hear from verification engineers about RTL linters isn't that they miss real violations — it's that they generate so many false positives that engineers stop trusting the output entirely. When a tool flags 800 warnings on a freshly checked-in block and 650 of them are style noise, the 150 genuine issues get buried. Engineers learn to ignore the tool, and the violations that actually matter sail through to simulation.

We've spent a lot of time thinking about why this happens and what the right fix looks like. The short answer: generic linting rules applied without architectural context are structurally unable to distinguish signal from noise. Fixing this isn't about tuning thresholds — it's about what information the linter has access to when it makes a decision.

What Makes a "False Positive" in RTL Linting

The term is slightly misleading in hardware design contexts. In software static analysis, a false positive is typically a code smell that's safe in practice — say, an unused variable that's intentionally declared as a placeholder. In RTL, the situations are more nuanced because the same construct can be correct or incorrect depending on design intent, target architecture, and the surrounding module hierarchy.

Consider Z-state output drives. A rule that flags tri-state bus assignments as violations will correctly catch accidental Z-propagation in most contexts — but it will also flag every intentional bus-hold architecture in a DDR PHY or an on-chip open-drain interface. The rule isn't wrong; the problem is that it has no way to distinguish between intentional and accidental use without knowing whether the output net is part of a bidirectional bus topology.

The same issue appears with inferred latches. A good linter flags every always @(*) block with incomplete sensitivity lists or missing else-branches that would infer a latch. But if you're designing a CPLD-style hold circuit or using an intentional level-sensitive element in a power management IP, those latches are load-bearing. Generic rules produce a warning; context-aware rules skip the warning and instead check that the latch behavior is what you actually intended.

The Architecture-Awareness Problem

Most off-the-shelf linters operate file-by-file or module-by-module. They parse the RTL, apply rule patterns to the parse tree, and report hits. This approach works well for truly local violations — things like casex with don't-care notation that causes X-propagation, or blocking assignments in sequential always blocks that should use non-blocking. Those violations are syntactically local.

But a significant fraction of the rules that matter most are cross-module. Whether a signal is properly synchronized before crossing a clock domain can't be determined from the driving module alone — you need to trace the fanout and see where it lands in the clock domain graph. Whether an output port width mismatch is a genuine bug or an intentional truncation depends on how the port is connected in the parent instantiation.

This is the fundamental limitation we hit when we were running standard lint flows on RTL for a mid-size ASIC team working on a high-speed serializer/deserializer IP block. The team was getting about 1,200 lint warnings per run. After manually triaging a sample, they found roughly 40% were either architectural choices they'd made deliberately, or cross-module situations the linter didn't have enough visibility to evaluate correctly. Another 30% were style preferences that their coding standards explicitly permitted. Only about 30% warranted real attention — but that 30% was mixed into the other 70% with no reliable way to sort automatically.

Rule Context Tuning: What Actually Works

There are three layers of tuning that consistently reduce false positive rates without increasing miss rates:

1. Module-Role Annotation

The single most effective intervention is giving the linter information about what a module is supposed to do. A PHY interface module, a clock generation block, an interrupt controller, and a FIFO buffer have completely different valid coding patterns. Latch inference is almost always wrong in a FIFO but may be deliberate in some clock gating cells. Tri-state signals are noise in a pure-digital datapath and meaningful in a bus interface.

We handle this through a lightweight module-role annotation system — a JSON sidecar file that labels each module's role in the hierarchy. Rules then filter against this metadata. A tri-state warning suppresses itself on modules labeled bus-interface; an inferred-latch warning suppresses itself on modules labeled clock-gate. The annotation takes maybe 20 minutes per project to fill in, and it eliminates a class of noise that's otherwise impossible to tune away.

2. Team Coding Standard Integration

Many false positives come from rules that enforce one style convention but are being applied to a team that has documented a different, equally valid convention. The classic example is named vs. positional port connections in instantiations. Some lint rules flag positional port connections as risky (which they are, at scale). But a team that uses positional connections consistently and has other guards in place is generating noise every time that rule fires.

The fix isn't to globally disable the rule — it's to make rule severity configurable per team. A rule that's an error for one team might be informational for another. When we built our rule configuration layer, we made severity a team-level property, not a tool-level property. That means a team can mark certain style rules as informational without affecting how the genuinely dangerous rules behave for them.

3. Historical Suppression Modeling

The third layer is where machine learning enters the picture. If a lint rule has been firing on a particular construct in a particular module for six months, and the team has reviewed and waived that warning every time, that pattern is worth learning. We track waivers and manual suppressions over time, and we use that history to build a per-team suppression model — a probabilistic classifier that asks, "given this rule firing on this construct in this module context, what's the likelihood that this team will waive it?"

Rules with high predicted waiver probability get flagged as "likely noise" in the report and sorted to the bottom. Rules with low predicted waiver probability get promoted. This doesn't replace human review — it reshapes the queue so engineers spend their first 20 minutes on the 40 violations most likely to be real, rather than wading through 400 warnings hoping to find the 40 that matter.

What We're Not Saying

We're not saying that false positive suppression should be aggressive to the point of hiding violations that haven't been explicitly reviewed. The goal is a tool you trust, not a tool that's quiet. A linter that suppresses too aggressively trains engineers to assume the clean report means clean RTL, which is dangerous.

The right mental model is a triage system, not a filter. Every violation is still evaluated. The output is a prioritized queue, not a shortened list. Engineers who want to see every warning, including the low-probability-waiver ones, can always expand the full report. The default view just starts with the highest-signal items.

Measuring Whether Tuning Is Working

One metric we track internally is the waiver rate per run — what fraction of lint warnings engineers manually suppress without taking corrective action. A waiver rate above 60% is a signal that the tool isn't calibrated to the team. Below 20% is the target zone, meaning most warnings the team sees are ones they act on.

A second metric is the violations-per-engineer-hour ratio in the review queue. If engineers are spending 2 hours per lint run clearing noise and 20 minutes on real issues, the tool is net-negative. If they're spending 20 minutes on noise and 2 hours on real issues, the math flips in the tool's favor.

The teams that have seen the biggest gains from architecture-aware linting aren't the ones who started with the most violations — they're the ones whose previous lint setup had eroded trust to the point where engineers were skipping lint runs before checkin. Getting those teams back to running lint on every commit, because they trust the output, is worth more than shaving 10 violations off an already-trusted run.

The State of the Tooling

Commercial EDA lint tools have made meaningful progress on configurability, but most of them still treat architecture context as an afterthought — something you add through waivers after the fact, not through upstream design intent metadata. The default behavior of most tools is still "apply all rules, report all hits, let the engineer sort it out."

That default behavior made sense when lint was a sign-off step run once a week by one person. It doesn't make sense when you're trying to run lint on every commit in a CI pipeline and have an engineer triage the output in under 30 minutes. The tooling needs to meet the workflow where it actually lives.

That's the gap we're building into. Not replacing engineer judgment, but making sure the tool's output is shaped to deserve it.