Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Digital Design & Verification (Verilog, SystemVerilog, VLSI)  Module 11 – Lint, synthesis, timing, power and test

Linting RTL with Verilator, slang and svlint

Run Verilator, slang and svlint on one buggy SystemVerilog file, read each warning, waive one with its reason, and make lint a gate before a merge.

  • Beginner
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Run Verilator --lint-only -Wall and interpret its warning classes
  • Add an LRM-strict check with slang and rule-based style checks with svlint
  • Write a waiver with its reason instead of switching a warning off globally
  • Set up lint as a gate in continuous integration

Before you start

What you’ll learn · Before you start

What you will learn

  • Run Verilator --lint-only -Wall and interpret its warning classes
  • Add an LRM-strict check with slang and rule-based style checks with svlint
  • Write a waiver with its reason instead of switching a warning off globally
  • Set up lint as a gate in continuous integration

Before you start

On this page

fan_ctrl.sv is a 31-line fan controller, and its directed test passes all 12 of its checks. Three linters read the same file without running it and report six problems, four of them bugs: at 46 °C the fan stops, at −5 °C it runs, full mode is not full and the overheat flag never rises.

Each linter reads code in its own way. Verilator reads the design as a simulator does, slang checks it against the language standard to the letter, and svlint checks the rules a team has chosen. Here you run all three on that file, read what each one reports, fix or waive every finding, and turn the three into a gate that a change must pass before it merges.

A passing test hid four bugs that lint reports

The controller sets a fan’s duty cycle, from 0 to 15 sixteenths of full speed, from the temperature and a mode: off, quiet, auto or full. Its author tested what they had in mind, twelve cases from 20 to 45 °C in the first three modes, and built the simulation the way their script always had, with Verilator’s lint warnings switched off:

The directed test, the cases it skipped and the fixed design

hdl/fan_ctrl.sv

// A fan controller: the duty cycle, 0 to 15 sixteenths of full speed, follows the temperature.
module fan_ctrl (
  input  logic              clk,
  input  logic              rst_n,
  input  logic signed [7:0] temp_c,   // degrees Celsius, -128 to 127
  input  logic        [7:0] start_c,  // the fan starts above this temperature
  input  logic        [1:0] mode,     // 0 off, 1 quiet, 2 auto, 3 full
  output logic        [3:0] duty,     // sixteenths of full speed
  output logic              hot       // above 80 degrees for 300 cycles
);
  logic [7:0] excess;                 // degrees above start_c
  logic [7:0] hot_count;

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)                excess = '0;
    else if (temp_c > start_c) excess = temp_c - start_c;
    else                       excess = '0;

  always_comb
    case (mode)
      2'd0: duty = 4'd0;
      2'd1: duty = excess[7:1];
      2'd2: duty = excess;
    endcase

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)               hot_count <= '0;
    else if (temp_c > 8'sd80) hot_count <= hot_count + 1'b1;

  assign hot = hot_count == 300;
endmodule

hdl/tb_fan_ctrl.sv

// The fan controller's directed test: the twelve cases its author thought of, then, with +skipped, four that the
// test left out. Expected duty: off 0; quiet half the degrees above start_c; auto the degrees above start_c; full
// 15; never more than 15.
`timescale 1ns / 1ps
module tb_fan_ctrl;
  logic              clk;
  logic              rst_n;
  logic signed [7:0] temp_c;
  logic signed [7:0] start_c;
  logic        [1:0] mode;
  logic        [3:0] duty;
  logic              hot;
  int                checks = 0, failed = 0;

  fan_ctrl dut (.clk(clk), .rst_n(rst_n), .temp_c(temp_c), .start_c(start_c), .mode(mode), .duty(duty), .hot(hot));

  initial clk = 1'b0;
  always #5 clk = ~clk;

  function automatic int expected(input int t, input int m);
    int above;
    above = (t > 30) ? t - 30 : 0;
    case (m)
      0:       return 0;
      1:       return (above / 2 > 15) ? 15 : above / 2;
      2:       return (above > 15) ? 15 : above;
      default: return 15;
    endcase
  endfunction

  task automatic check(input int t, input int m);
    int want;
    want   = expected(t, m);
    temp_c = 8'(t);
    mode   = 2'(m);
    @(posedge clk);
    #1;
    checks++;
    if (int'(duty) != want) begin
      failed++;
      $display("FAIL %4d C, mode %0d: duty %2d, expected %2d", t, m, duty, want);
    end
  endtask

  initial begin
    rst_n   = 1'b0;
    temp_c  = 8'sd20;
    start_c = 8'sd30;
    mode    = 2'd0;
    #12 rst_n = 1'b1;
    for (int m = 0; m < 3; m++) begin
      check(20, m);
      check(30, m);
      check(40, m);
      check(45, m);
    end
    if ($test$plusargs("skipped")) begin
      check(46, 2);
      check(-5, 2);
      check(40, 3);
      temp_c = 8'sd85;
      repeat (300) @(posedge clk);
      #1;
      checks++;
      if (hot !== 1'b1) begin
        failed++;
        $display("FAIL   85 C for 300 cycles: hot %b, expected 1", hot);
      end
    end
    $display("%0d of %0d checks passed", checks - failed, checks);
    $finish;
  end
endmodule

Commands · hdl/tests_session.py

from session import run

# The directed test as its author ran it: Verilator builds the simulation with its lint warnings switched off
# (-Wno-lint), the twelve checks pass, and +skipped adds the four cases the test left out. Then the fixed design,
# built with every warning on.
run("""
verilator --binary -j 0 --quiet -Wno-lint --top-module tb_fan_ctrl tb_fan_ctrl.sv fan_ctrl.sv
obj_dir/Vtb_fan_ctrl +verilator+quiet
obj_dir/Vtb_fan_ctrl +verilator+quiet +skipped
verilator --binary -j 0 --quiet -Wall --top-module tb_fan_ctrl -Mdir obj_fixed tb_fan_ctrl.sv fixed/fan_ctrl.sv
obj_fixed/Vtb_fan_ctrl +verilator+quiet +skipped
""")

Output

# Verilator 5.052
$ verilator --binary -j 0 --quiet -Wno-lint --top-module tb_fan_ctrl tb_fan_ctrl.sv fan_ctrl.sv
$ obj_dir/Vtb_fan_ctrl +verilator+quiet
12 of 12 checks passed
- tb_fan_ctrl.sv:71: Verilog $finish
$ obj_dir/Vtb_fan_ctrl +verilator+quiet +skipped
FAIL   46 C, mode 2: duty  0, expected 15
FAIL   -5 C, mode 2: duty 13, expected  0
FAIL   40 C, mode 3: duty 13, expected 15
FAIL   85 C for 300 cycles: hot 0, expected 1
12 of 16 checks passed
- tb_fan_ctrl.sv:71: Verilog $finish
$ verilator --binary -j 0 --quiet -Wall --top-module tb_fan_ctrl -Mdir obj_fixed tb_fan_ctrl.sv fixed/fan_ctrl.sv
$ obj_fixed/Vtb_fan_ctrl +verilator+quiet +skipped
16 of 16 checks passed
- tb_fan_ctrl.sv:71: Verilog $finish

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tests_session.py

All twelve checks passed. The four cases the test left out failed: 46 °C in auto mode gave a duty of 0, −5 °C gave 13, full mode kept the 13 it had, and 300 cycles at 85 °C never raised hot. None of them is exotic; each is one input the author did not try.

lint: Checking source code without running it for constructs that are legal but suspicious, such as a signal that is never read or a width that does not match; a lint tool reports them as warnings. More in “How chips are designed and verified” finds such bugs from the code alone, because each one leaves a mark that a reader can see: an 8-bit value assigned to a 4-bit output, a case with a missing value, a signed number compared with an unsigned one, an 8-bit counter compared with 300. A test finds a bug only when it happens to drive the input that shows it; a linter reads every line on every run. The last run above is the same test on fixed/fan_ctrl.sv, the version that every linter of this lesson passes: 16 of 16 checks.

Switching lint off to get a simulation built

The test script built the simulation with -Wno-lint, because Verilator stops a build on warnings and these were in the way. That one flag hid every bug in the file from the only tool that was running. Build with the warnings on, and fix or waive each one.

Checkpoint

Check: Why a passing test still needs lint

  1. Question 1 of 2 The directed test of fan_ctrl.sv passed 12 of 12 checks, yet 46 °C in auto mode gives a duty of 0. Why did the test not see it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No check drove a temperature more than 15 degrees above start_c in auto mode, where the duty wraps

    A test sees a bug only when it drives the input that triggers it. A linter reads the 8-bit assignment to the 4-bit duty on every run, whatever the inputs.

  2. Question 2 of 2 What does a linter use to find a problem in RTL?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The code alone, read and elaborated without any input vectors

    Lint reads the source and checks it against rules about widths, cases, assignments and style, so it needs no stimulus and runs in seconds.

Verilator reports lint warnings by default, and style warnings when asked

verilator --lint-only reads and elaborates the design as it would for a simulation, then stops before it writes any C++. Its manual describes the option as a check for lint violations that creates no other output, and -Wall adds the messages it considers stylistic, the style warning (Verilator): A Verilator warning about a way of writing code that tends to cause mistakes later, such as BLKSEQ for a blocking = in a clocked block. Style warnings are off by default and switched on with -Wall., which are off by default. Here is fan_ctrl.sv with every class on, then a count of the warnings with and without -Wall:

Verilator's lint, with every warning class on Python · hdl/verilator_session.py
from session import run

# Verilator's lint with every warning class on (-Wall), then how many warnings each setting reports. With -Wno-fatal
# the warnings are still printed, but the command succeeds, so a script that reads only the exit status sees none.
run("""
!verilator --lint-only -Wall fan_ctrl.sv
verilator --lint-only fan_ctrl.sv 2>&1 | grep -c "^%Warning"
verilator --lint-only -Wall -Wno-fatal fan_ctrl.sv > lint.log 2>&1
grep -c "^%Warning" lint.log
""")

Output

# Verilator 5.052
$ verilator --lint-only -Wall fan_ctrl.sv
%Warning-WIDTHTRUNC: fan_ctrl.sv:22:18: Operator ASSIGN expects 4 bits on the Assign RHS, but Assign RHS's SEL generates 7 bits.
                                      : ... note: In instance 'fan_ctrl'
   22 |       2'd1: duty = excess[7:1];
      |                  ^
                     ... For warning description see https://verilator.org/warn/WIDTHTRUNC?v=5.052
                     ... Use "/* verilator lint_off WIDTHTRUNC */" and lint_on around source to disable this message.
%Warning-WIDTHTRUNC: fan_ctrl.sv:23:18: Operator ASSIGN expects 4 bits on the Assign RHS, but Assign RHS's VARREF 'excess' generates 8 bits.
                                      : ... note: In instance 'fan_ctrl'
   23 |       2'd2: duty = excess;
      |                  ^
%Warning-WIDTHEXPAND: fan_ctrl.sv:30:26: Operator EQ expects 32 or 9 bits on the LHS, but LHS's VARREF 'hot_count' generates 8 bits.
                                       : ... note: In instance 'fan_ctrl'
   30 |   assign hot = hot_count == 300;
      |                          ^~
                      ... For warning description see https://verilator.org/warn/WIDTHEXPAND?v=5.052
                      ... Use "/* verilator lint_off WIDTHEXPAND */" and lint_on around source to disable this message.
%Warning-BLKSEQ: fan_ctrl.sv:15:39: Blocking assignment '=' in sequential logic process
                                  : ... note: In instance 'fan_ctrl'
                                  : ... Suggest using delayed assignment '<='
   15 |     if (!rst_n)                excess = '0;
      |                                       ^
                 ... For warning description see https://verilator.org/warn/BLKSEQ?v=5.052
                 ... Use "/* verilator lint_off BLKSEQ */" and lint_on around source to disable this message.
%Warning-BLKSEQ: fan_ctrl.sv:16:39: Blocking assignment '=' in sequential logic process
                                  : ... note: In instance 'fan_ctrl'
                                  : ... Suggest using delayed assignment '<='
   16 |     else if (temp_c > start_c) excess = temp_c - start_c;
      |                                       ^
%Warning-BLKSEQ: fan_ctrl.sv:17:39: Blocking assignment '=' in sequential logic process
                                  : ... note: In instance 'fan_ctrl'
                                  : ... Suggest using delayed assignment '<='
   17 |     else                       excess = '0;
      |                                       ^
%Warning-CASEINCOMPLETE: fan_ctrl.sv:20:5: Case values incompletely covered (example pattern 0x3)
   20 |     case (mode)
      |     ^~~~
                         ... For warning description see https://verilator.org/warn/CASEINCOMPLETE?v=5.052
                         ... Use "/* verilator lint_off CASEINCOMPLETE */" and lint_on around source to disable this message.
%Warning-LATCH: fan_ctrl.sv:19:3: Latch inferred for signal 'duty' (not all control paths of combinational always assign a value)
                                : ... Suggest use of always_latch for intentional latches
   19 |   always_comb
      |   ^~~~~~~~~~~
                ... For warning description see https://verilator.org/warn/LATCH?v=5.052
                ... Use "/* verilator lint_off LATCH */" and lint_on around source to disable this message.
%Error: Exiting due to 8 warning(s)
[exit status 1]
$ verilator --lint-only fan_ctrl.sv 2>&1 | grep -c "^%Warning"
5
$ verilator --lint-only -Wall -Wno-fatal fan_ctrl.sv > lint.log 2>&1
$ grep -c "^%Warning" lint.log
8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 verilator_session.py

Each warning starts with its code, then the file, line and column, then what Verilator expected and what it found: Operator ASSIGN expects 4 bits on the Assign RHS, but Assign RHS's VARREF 'excess' generates 8 bits. The excerpt under it marks the place, and the last two lines say where the code is described and how to waive it. Read the code first, because it names the kind of mistake:

Warning Class What it found in fan_ctrl.sv
WIDTHTRUNC lint, on by default 7 and 8 bits assigned to the 4-bit duty, lines 22 and 23
WIDTHEXPAND lint, on by default the 8-bit hot_count compared with 300, which needs 9 bits
CASEINCOMPLETE lint, on by default no case item for mode 3
LATCH lint, on by default duty keeps its last value in mode 3, which is a latch
BLKSEQ style, with -Wall blocking = in the clocked block, lines 15 to 17

Without -Wall Verilator reported 5 warnings; with it, 8, the three BLKSEQ lines added. The lint class is about what the code computes, the style class about ways of writing that cause mistakes later. BLKSEQ is harmless in this file today, because only the combinational block reads excess, and it becomes a race on the day another clocked block reads it.

The -Wall run ended with %Error: Exiting due to 8 warning(s) and exit status: The number a program returns when it ends: 0 for success, any other value for failure. Scripts and continuous integration jobs read it to decide whether to go on. 1, and that status is what a script can act on. -Wno-fatal keeps the messages and returns 0: the run with it wrote all 8 warnings into lint.log and still succeeded, so a job that reads only the exit status would have passed the file.

Checkpoint

Check: Verilator's lint and style warnings

  1. Question 1 of 2 Verilator printed "%Warning-WIDTHEXPAND: fan_ctrl.sv:30:26: Operator EQ expects 32 or 9 bits on the LHS, but LHS's VARREF 'hot_count' generates 8 bits." What is wrong with line 30?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An 8-bit counter is compared with 300, which needs 9 bits, so the comparison can never be true

    Read the code and the two widths: the 8-bit side cannot hold the 9-bit constant. The fix counts in 9 bits and stops at 300.

  2. Question 2 of 2 Verilator reported 5 warnings for fan_ctrl.sv without -Wall. How many did it report with -Wall?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 8 warnings

    -Wall adds the style class. Here that was BLKSEQ on lines 15, 16 and 17, three warnings more than the five of the lint class.

slang and svlint report what Verilator does not

slang is a SystemVerilog compiler front end whose stated aim is complete support of the 1800-2023 standard, and its warnings come in groups: -Wextra adds width and latch checks to a small default set, and a single warning can be named, such as -Wsign-compare. svlint checks each file’s syntax tree against rules that a team switches on in a file named .svlint.toml unless --config names another, svlint.toml here; its manual says every rule is off until it is enabled there. Yosys, this track’s synthesis tool, runs too, because synthesis refuses some code outright:

slang, svlint and Yosys on fan_ctrl.sv

hdl/svlint.toml

# The project's svlint rules: every rule is off until this file switches it on.
[option]
textwidth = 100

[textrules]
style_textwidth = true

[syntaxrules]
default_nettype_none = true
blocking_assignment_in_always_ff = true
case_default = true
keyword_forbidden_always = true

Commands · hdl/tools_session.py

from session import run

# The same file through slang, svlint and synthesis. slang reports warnings as errors here (-Werror), so that its
# exit status says whether it found anything; svlint reads the project's rules from svlint.toml.
run("""
!slang -Wextra -Wsign-compare -Werror fan_ctrl.sv
!svlint --config svlint.toml fan_ctrl.sv
!yosys -q -p "read_verilog -sv fan_ctrl.sv; proc"
""")

Output

# slang 12.0.0 · svlint 0.9.5 · Yosys 0.69
$ slang -Wextra -Wsign-compare -Werror fan_ctrl.sv
fan_ctrl.sv:16:21: error: comparison of differently signed types ('logic signed[7:0]' and 'logic[7:0]') [-Wsign-compare]
    else if (temp_c > start_c) excess = temp_c - start_c;
             ~~~~~~ ^ ~~~~~~~
fan_ctrl.sv:22:20: error: implicit conversion truncates from 7 to 4 bits [-Wwidth-trunc]
      2'd1: duty = excess[7:1];
                 ~ ^~~~~~~~~~~
fan_ctrl.sv:23:20: error: implicit conversion truncates from 8 to 4 bits [-Wwidth-trunc]
      2'd2: duty = excess;
                 ~ ^~~~~~
fan_ctrl.sv:21:13: error: latch inferred for 'duty' because it is not assigned on all control paths [-Winferred-latch]
      2'd0: duty = 4'd0;
            ^~~~
Top level design units:
    fan_ctrl


Build failed: 4 errors, 0 warnings
[exit status 1]
$ svlint --config svlint.toml fan_ctrl.sv
Fail: default_nettype_none
   --> fan_ctrl.sv:2:1
  |
2 | module fan_ctrl (
  | ^^^^^^ hint  : Place `` `default_nettype none`` at the top of source code.
  |        reason: Compiler directive `` `default_nettype none`` detects unintentional implicit wires.

Fail: blocking_assignment_in_always_ff
   --> fan_ctrl.sv:14:3
   |
14 |   always_ff @(posedge clk or negedge rst_n)
   |   ^^^^^^^^^ hint  : Do not use blocking assignments within `always_ff`.
   |             reason: Blocking assignment in `always_ff` may cause undefined event ordering.

Fail: case_default
   --> fan_ctrl.sv:20:5
   |
20 |     case (mode)
   |     ^^^^ hint  : Use a `default` expression in `case` statements.
   |          reason: Incomplete case may cause simulation/synthesis mismatch in `always_comb` and `function`.
[exit status 1]
$ yosys -q -p "read_verilog -sv fan_ctrl.sv; proc"
ERROR: Latch inferred for signal `\fan_ctrl.\duty' from always_comb process `\fan_ctrl.$proc$fan_ctrl.sv:19$5'.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tools_session.py

slang’s first error is the bug that no other tool reported. temp_c is signed and start_c is not, and the standard’s rule for an expression’s type (IEEE 1800-2023, 11.8.1) makes the whole comparison unsigned when one operand is unsigned. −5 °C is the bit pattern 251, which is greater than 30, so the fan starts, and 251 − 30 = 221 leaves 13 in the four bits of duty: the 13 of the failed check. svlint found three breaks of the team’s rules. One of them, the missing `default_nettype none, is no bug today, but without it a misspelt name becomes a new 1-bit wire instead of an error. Yosys stopped at the latch, so this file would never reach a netlist: A list of the cells a design uses and the wires that connect them, written by synthesis and read by place and route and by timing analysis. More in “How chips are designed and verified”.

Finding Reported by
signed temp_c against unsigned start_c slang: sign-compare
7 and 8 bits into the 4-bit duty Verilator: WIDTHTRUNC; slang: width-trunc
no item for mode 3, a latch Verilator: CASEINCOMPLETE and LATCH; slang: inferred-latch; svlint: case_default; Yosys: stopped
8-bit count compared with 300 Verilator: WIDTHEXPAND
blocking = in always_ff Verilator: BLKSEQ; svlint: blocking_assignment_in_always_ff
no `default_nettype none svlint: default_nettype_none

Read the table down the right-hand column: each of the three linters has a row that only it reports, and each misses something. That is why a project runs more than one linter.

Key idea

Each linter reads the code a different way, so each finds problems the others miss: Verilator the widths, slang the standard’s rules, svlint the team’s own conventions. Run all three.

Checkpoint

Check: What slang and svlint add

  1. Question 1 of 2 temp_c is logic signed [7:0] and start_c is logic [7:0], holding 30. What does temp_c > start_c give when temp_c is −5, and why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1, because one unsigned operand makes the comparison unsigned, and −5 is the bit pattern 251

    The fan therefore starts at −5 °C, and 251 − 30 = 221 leaves 13 in the duty's four bits. Declaring start_c signed makes the comparison signed.

  2. Question 2 of 2 A team runs only Verilator -Wall. Which findings of fan_ctrl.sv would it never see? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • The signed temp_c compared with the unsigned start_c
    • The missing default_nettype none

    Each linter has findings only it reports: here slang the sign rule, svlint the team's convention and Verilator the 300 comparison.

A waiver names one warning, one place and the reason for it

Most findings are fixed in the code, and the fix usually says more than the old code did: fixed/fan_ctrl.sv compares two signed values, saturates the duty before it takes excess[3:0], counts to 300 in 9 bits and gives mode 3 its own item. A warning about something the design really wants, such as a truncation, is often best answered with a cast, 16'(now_us), which states the intent where it applies and leaves nothing to waive.

When the code cannot change, a lint waiver: A comment or a configuration line that turns off one lint warning at one named place in the code, with the reason for it. A flag that turns a warning off for a whole design hides every later instance of it too. More in “RTL coding style that scales” is the tool, and it should be as narrow as it can be: one rule, one place and a comment that says why. Each tool has its own comment form:

  • Verilator: /* verilator lint_off WIDTHTRUNC */ before the line and /* verilator lint_on WIDTHTRUNC */ after it.
  • slang: // slang lint_off width-trunc and // slang lint_on width-trunc.
  • svlint: /* svlint off case_default */ and /* svlint on case_default */, for its syntax rules.

For a file you may not edit, such as a supplied block, Verilator reads a control file (Verilator): A file that begins with `verilator_config and holds commands such as lint_off -rule WIDTHTRUNC -file "*x.sv" -lines 10, so that a warning in a file you may not edit is waived without changing the file. with the same waiver for a named file and line: lint_off -rule WIDTHTRUNC -file "*stamp_legacy.sv" -lines 10. Its manual says that such a waiver applies only to files read after the control file, so the control file goes first on the command line.

A switch on the command line, -Wno-WIDTHTRUNC, is a waiver for every line of every file, the lines written next year included. Walk through what that did in one project:

Timeline · 5 events in order How two global switches let a bug merge Walk through the project's changes in order. The drill below replays the last two with the real files.
  1. Change 1

    A supplied block warns

    stamp_legacy.sv keeps the low 16 bits of a 32-bit time on purpose, so that its stamp wraps. Verilator reports WIDTHTRUNC on line 10 and UNUSEDSIGNAL on line 6, and the lint job fails.

  2. Change 2

    The two warnings go off everywhere global switch

    To get the job passing, -Wno-WIDTHTRUNC and -Wno-UNUSEDSIGNAL are added to the lint command. The supplied file is quiet now, and so is every file that anyone writes later.

  3. Change 3

    A new truncation arrives

    fan_override.sv sets the duty from a percentage, pct * 16 / 100. At 100 percent that is 16, which needs five bits, so the four bits of duty keep 0 and the fan stops.

  4. Change 4

    Lint stays quiet, and it merges

    The global switches hide the new WIDTHTRUNC as well, so the change passes lint and merges. Nothing in the job's output says that a warning was ever there.

  5. Change 5

    The switches become a waiver scoped waiver

    A control file, waivers.vlt, waives the two warnings for line 10 and line 6 of stamp_legacy.sv only. The next run reports line 10 of fan_override.sv, and the change is stopped until the duty saturates.

The timeline has 5 events, from Change 1 to Change 5.

The drill replays the last two changes: the old command with its two global switches, then the control file that waives the two warnings for the supplied file’s two lines only.

Two global switches, then a scoped waiver

hdl/stamp_legacy.sv

`default_nettype none
// A supplied timestamp block: it keeps the low 16 bits of a 32-bit time on purpose, so that the stamp wraps.
module stamp_legacy (
  input  logic        clk,
  input  logic        take,
  input  logic [31:0] now_us,
  output logic [15:0] stamp
);
  always_ff @(posedge clk)
    if (take) stamp <= now_us;
endmodule
`default_nettype wire

hdl/fan_override.sv

`default_nettype none
// A later change: the fan's speed can be set by hand, in percent.
module fan_override (
  input  logic       clk,
  input  logic       set,          // a new setting arrives
  input  logic [6:0] pct,          // 0 to 100 percent
  output logic [3:0] duty          // sixteenths of full speed
);
  always_ff @(posedge clk)
    if (set) duty <= pct * 16 / 100;
endmodule
`default_nettype wire

hdl/waivers.vlt

`verilator_config
// stamp_legacy.sv is supplied and not ours to edit: it keeps the low 16 bits of the time on purpose (its spec, 4.2).
lint_off -rule WIDTHTRUNC -file "*stamp_legacy.sv" -lines 10
lint_off -rule UNUSEDSIGNAL -file "*stamp_legacy.sv" -lines 6

Commands · hdl/drill_session.py

from session import run

# The project's old lint command switched two warnings off for every file, to quiet the supplied stamp_legacy.sv.
# Then fan_override.sv arrived. The new command waives the two warnings for the supplied file's two lines only.
run("""
for f in stamp_legacy.sv fan_override.sv; do verilator --lint-only -Wall --quiet -Wno-WIDTHTRUNC -Wno-UNUSEDSIGNAL $f; done
!for f in stamp_legacy.sv fan_override.sv; do verilator --lint-only -Wall --quiet waivers.vlt $f || exit 1; done
""")

Output

# Verilator 5.052
$ for f in stamp_legacy.sv fan_override.sv; do verilator --lint-only -Wall --quiet -Wno-WIDTHTRUNC -Wno-UNUSEDSIGNAL $f; done
$ for f in stamp_legacy.sv fan_override.sv; do verilator --lint-only -Wall --quiet waivers.vlt $f || exit 1; done
%Warning-WIDTHTRUNC: fan_override.sv:10:19: Operator ASSIGNDLY expects 4 bits on the Assign RHS, but Assign RHS's DIV generates 32 or 7 bits.
                                          : ... note: In instance 'fan_override'
   10 |     if (set) duty <= pct * 16 / 100;
      |                   ^~
                     ... For warning description see https://verilator.org/warn/WIDTHTRUNC?v=5.052
                     ... Use "/* verilator lint_off WIDTHTRUNC */" and lint_on around source to disable this message.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 drill_session.py

The old command printed nothing for either file, so the bug in fan_override.sv would have merged unseen: 100 × 16 / 100 = 16, which needs five bits, and the four bits of duty keep 0, so the override turns the fan off at 100 %. The scoped waiver kept the supplied file quiet and reported line 10 of the new one.

A -Wno flag to quiet one file

-Wno-WIDTHTRUNC in the lint command waives the warning in files that nobody has written yet. Waive the line, or the supplied file in a control file, and keep the reason next to it.

Checkpoint

Check: Narrow waivers with reasons

  1. Question 1 of 2 stamp_legacy.sv is supplied, may not be edited, and warns WIDTHTRUNC on line 10 on purpose. Which waiver keeps every other truncation in the project visible?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A control file read first, with lint_off -rule WIDTHTRUNC -file "*stamp_legacy.sv" -lines 10

    Waive the rule at the place that needs it, with the reason beside it. For files you may not edit, Verilator's control file names the file and lines; put it before the files it applies to.

  2. Question 2 of 2 fan_override.sv sets duty <= pct * 16 / 100 in a 4-bit duty. What value does duty hold for pct = 100?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 0 sixteenths

    100 × 16 / 100 = 16, which is 1_0000 in binary, five bits. The four bits of duty keep 0000, so the override turns the fan off at full speed, which WIDTHTRUNC reports once no global switch hides it.

Lint becomes a gate when its exit status stops a merge

A linter that someone runs when they remember protects nothing. A gate runs on every change, the same way on every computer, and a change merges only when the gate ends with exit status 0. lint.sh is this project’s gate: it runs all three linters on the files it is given, so that one run lists everything to fix, and it fails if any of them fails.

The lint gate on the original and on the fixed design

hdl/lint.sh

#!/bin/sh
# The lint gate: three linters on the files given. Each must be clean, or the gate fails (exit status 1) and the
# change does not merge. Every linter runs, so one run shows everything that needs fixing.
status=0
verilator --lint-only -Wall --quiet waivers.vlt "$@" > verilator.log 2>&1 ||
  { echo "verilator: $(grep -c '^%Warning' verilator.log) warnings"; status=1; }
slang -q -Wextra -Wsign-compare -Werror "$@" > slang.log 2>&1 ||
  { echo "slang: $(grep -c ': error:' slang.log) errors"; status=1; }
svlint --config svlint.toml "$@" > svlint.log 2>&1 ||
  { echo "svlint: $(grep -c '^Fail' svlint.log) rule failures"; status=1; }
if [ "$status" -eq 0 ]; then echo "lint gate: clean"; fi
exit "$status"

Commands · hdl/gate_session.py

from session import run

# The gate on the original file, then on the fixed one.
run("""
!sh lint.sh fan_ctrl.sv
sh lint.sh fixed/fan_ctrl.sv
""")

Output

# Verilator 5.052 · slang 12.0.0 · svlint 0.9.5
$ sh lint.sh fan_ctrl.sv
verilator: 8 warnings
slang: 4 errors
svlint: 3 rule failures
[exit status 1]
$ sh lint.sh fixed/fan_ctrl.sv
lint gate: clean

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gate_session.py

On the original file the gate reported 8 Verilator warnings, 4 slang errors and 3 svlint rule failures and ended with status 1; on the fixed file it printed lint gate: clean. Each tool’s exit status needs care. Verilator fails on any warning unless it is given -Wno-fatal, so the gate never gives it that. slang fails only on errors, so the gate passes -Werror, which its manual describes as treating all warnings as errors; without it the four findings would have printed and passed. svlint fails on any rule failure.

In a project the same script runs in two places: on the engineer’s computer before a commit, and in the continuous integration (CI): A service that builds and checks every change pushed to a shared repository, such as a pull request, so that a change that fails a check is stopped before it merges. job of every pull request, which the repository can be set to require before a merge. Step through one change on its way to the main branch:

Interactive · stage by stage One change on its way through the lint gate Step through a change from the engineer's computer to the main branch. A failing stage sends it back.
One change on its way through the lint gate, step 8 of 8. Showing Change the RTL, Run lint.sh first, Push a pull request, CI runs lint.sh, CI runs the tests, A teammate reads it, Merge. In focus: Merge.any findingChange the RTLon your own computerRun lint.sh firstseconds, before the commitPush a pull requestthe change and its waiversCI runs lint.shall three lintersCI runs the teststhe simulation regressionA teammate reads itevery waiver and its reasonMergeonly when all pass

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The change. An engineer edits fan_ctrl.sv. Nothing has been checked yet, and the test they wrote covers the cases they had in mind.
  2. Lint before the commit. They run lint.sh on their own computer first. It takes seconds, so it can run before every commit, and it is the same script that the CI job runs.
  3. The pull request. The change goes to the team with any waiver it adds written in the code, next to the line it waives and with its reason.
  4. The gate. The CI job runs lint.sh on the change. On the original fan_ctrl.sv it printed 8 Verilator warnings, 4 slang errors and 3 svlint rule failures, and ended with exit status 1.
  5. Stopped. Exit status 1 stops the change here, before anyone spends time reading it. It goes back to the engineer with the gate's list of findings.
  6. Clean. On fixed/fan_ctrl.sv the gate printed "lint gate: clean" and ended with status 0, so the job goes on to the simulation regression.
  7. A teammate reads it. A teammate reads the change and every waiver in it. A waiver without a reason, or one wider than the line it needs, is sent back like a failing test.
  8. Merge. The change merges only when the gate and the tests have passed and the teammate has accepted it, and the same gate runs again on the next change.

The gate earns its place only while it stays fast and strict: seconds per change, no finding allowed, and every waiver written in the code, next to the line it waives, where the next reader of the change sees it.

Checkpoint

Check: Lint as a gate before a merge

  1. Question 1 of 2 Without -Werror, slang printed the four findings of fan_ctrl.sv as warnings. What would lint.sh have done?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Passed the slang stage, because slang ends with status 0 when it reports only warnings

    A gate trusts exit statuses, so each tool must fail on what the gate forbids: Verilator by default, slang with -Werror, svlint on any rule failure.

  2. Question 2 of 2 A team runs lint.sh only once a week, on the main branch. What does that miss that a gate on every pull request catches?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Each change's findings before it merges, while its author still has it in mind

    A gate is a check that runs before the merge and can stop it. The same script also runs on the engineer's computer, so most findings never leave it.

Practise: audit a file’s waivers

A gate can check the waivers too. The exercise writes the check that every waiver names its rule, gives its reason and is closed again; the sample tests use the waiver forms of this lesson:

Exercise · Easy · Python

Audit the lint waivers of a file

A project writes its Verilator waivers as block comments, each with its reason in a line comment after it:

/* verilator lint_off WIDTHTRUNC */ // the stamp wraps on purpose

Write audit(source), the check its lint gate runs on every file. It takes the text of a file and returns a list of (line, problem) tuples, with lines numbered from 1, sorted by line:

  • "no rule": a lint_off or lint_on that names no rule, such as /* verilator lint_off */, which would turn every warning off. It is reported and otherwise ignored.
  • "no reason": a lint_off with no // comment after it on its line, or one with nothing but spaces in it.
  • "nothing to close": a lint_on of a rule that has no open lint_off.
  • "never closed": a lint_off still open at the end of the file, reported at the line of that lint_off.

A rule's name is upper-case letters, digits and underscores. A lint_on closes the most recent open lint_off of the same rule. A clean file gives [].

Starter code · audit.py

def audit(source):
    """The problems of the file's Verilator waivers, as (line, problem) tuples sorted by line."""
    return []
The sample tests · test_audit.py
from audit import audit

STAMP = """always_ff @(posedge clk)
  /* verilator lint_off WIDTHTRUNC */  // the stamp wraps on purpose
  if (take) stamp <= now_us;
  /* verilator lint_on WIDTHTRUNC */
"""


def test_clean_waiver():
    """accepts a waiver that names its rule, gives its reason and is closed"""
    assert audit(STAMP) == []
    assert audit("") == []


def test_no_reason():
    """reports a lint_off with no reason after it, or an empty one"""
    src = "/* verilator lint_off UNUSEDSIGNAL */\nlogic [7:0] spare;\n/* verilator lint_on UNUSEDSIGNAL */\n"
    assert audit(src) == [(1, "no reason")]
    assert audit(src.replace("*/\nlogic", "*/ //   \nlogic")) == [(1, "no reason")]


def test_never_closed():
    """reports a lint_off left open at the end of the file, at its own line"""
    src = "logic a;\n/* verilator lint_off WIDTHTRUNC */  // supplied code\nassign y = a;\n"
    assert audit(src) == [(2, "never closed")]


def test_no_rule():
    """reports a waiver without a rule, which would switch every warning off"""
    assert audit("/* verilator lint_off */  // quiet, please\n") == [(1, "no rule")]


def test_nothing_to_close():
    """reports a lint_on that closes nothing, here because the rules differ"""
    src = ("/* verilator lint_off WIDTHTRUNC */  // supplied code\nassign y = a;\n"
           "/* verilator lint_on WIDTHEXPAND */\n")
    assert audit(src) == [(1, "never closed"), (3, "nothing to close")]


def test_two_rules():
    """keeps the open waivers of each rule apart, and closes the latest one of the same rule"""
    src = ("/* verilator lint_off WIDTHTRUNC */  // a\n"
           "/* verilator lint_off UNUSEDSIGNAL */  // b\n"
           "/* verilator lint_on WIDTHTRUNC */\n"
           "/* verilator lint_off WIDTHTRUNC */  // c\n"
           "/* verilator lint_on UNUSEDSIGNAL */\n"
           "/* verilator lint_on WIDTHTRUNC */\n")
    assert audit(src) == []
A hint

Find each waiver with one regular expression that captures lint_off or lint_on and an optional rule name. Keep a list of the open lines for each rule: a lint_off appends its line, a lint_on takes the last one off.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then check yourself on the five ideas, from the test that passed to the gate that stops a merge. The questions start where your answers have been weakest so far:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Why can lint find a bug that a passing directed test missed?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: It reads every line of the code, whatever inputs the test happened to drive

    Lint covers every line on every run, and the test only the cases its author thought of.

  2. Question 2 of 10 A colleague says: "Our regression has 2,000 tests and 98 % code coverage, so lint adds nothing." Which answer shows the gap?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Coverage counts lines that ran, not values that break them; the duty line ran in fan_ctrl.sv's test and still wrapped at 46 °C

    Code coverage measures what ran; a truncation runs on every pass and is wrong only for some values, which lint reports from the widths.

  3. Question 3 of 10 What does -Wall add to verilator --lint-only?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The style warnings, such as BLKSEQ, which are off by default

    Lint warnings are on by default; -Wall adds the style class.

  4. Question 4 of 10 A CI job runs verilator --lint-only -Wall -Wno-fatal and checks the exit status. What happens to fan_ctrl.sv?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: It passes, although Verilator prints all 8 warnings, because -Wno-fatal makes the run end with status 0

    A gate reads the exit status, so -Wno-fatal turns it into a check that never fails.

  5. Question 5 of 10 Which rules does svlint check when the project has a .svlint.toml with four rules set to true?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: Exactly those four; every other rule is off until enabled

    svlint's rules are a team's choice. Without any configuration file it enables every rule at once, including rules that contradict each other.

  6. Question 6 of 10 You must keep start_c an unsigned 8-bit register field. Which change makes temp_c > start_c correct for every temperature from −128 to 127?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Compare in 9 signed bits, $signed({1'b0, start_c}), so both sides are signed and 30 stays 30

    Make both operands signed without changing either value: widen the unsigned one with a 0 bit and cast it to signed.

  7. Question 7 of 10 What does a good lint waiver contain?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: One rule, the one place it applies to, and the reason

    Narrow and explained: one rule, one place, one reason.

  8. Question 8 of 10 A supplied block raises 40 UNUSEDSIGNAL warnings on ports it does not use. You may not edit it. What do you write?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: A control-file waiver for that rule and that file only, read before the file, with the reason in a comment

    A control file waives a rule for a file pattern and, where needed, for lines; it must come before the files it applies to on the command line.

  9. Question 9 of 10 What turns a lint run into a gate?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: It runs on every change, and a non-zero exit status stops the change from merging

    A gate is automatic and binding; it runs everywhere and its exit status decides.

  10. Question 10 of 10 lint.sh runs Verilator, slang and svlint and only then reports. Why not stop at the first tool that fails?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: So that one run lists every finding of every tool, and the author fixes them all at once

    A gate that reports everything at once saves rounds of fix and rerun; its exit status still fails on any finding.

Interview questions

Why lint RTL when its testbench already passes?

A test checks only the inputs it drives, and a linter reads every line. In this lesson’s fan controller the directed test passed 12 of 12 checks while three linters reported six problems, four of them bugs: a truncated duty that stopped the fan at 46 °C, an unsigned comparison that ran it at −5 °C, a missing case item that made a latch, and a count that could never reach 300. Lint takes seconds and runs on every change, so it is the cheapest check a design has. It does not replace tests: it says whether the code is written the way its author most likely meant, not whether the design does what its specification asks.

Two harder questions follow, a global switch that let a bug through and a lint gate for a whole team:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your project's lint command has carried -Wno-WIDTHTRUNC for two years, added to quiet a supplied block. A width truncation in new RTL has just reached the lab. What do you change in the flow so that this cannot happen again, without editing the supplied block, and how do you find what else the switch has hidden?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    Design the lint gate for a team of 40 RTL engineers that uses Verilator, slang and svlint. Which flags does each tool get, where does the gate run, how are waivers controlled, and how do you keep it fast enough to run on every change?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does verilator --lint-only do, and what does -Wall add?

    It reads and elaborates the design as for a simulation, then stops before it writes any C++. -Wall adds the style warnings, which are off by default, such as BLKSEQ.

  2. Which lint warnings did Verilator report on fan_ctrl.sv by default?

    WIDTHTRUNC for 7 and 8 bits into the 4-bit duty, WIDTHEXPAND for an 8-bit count compared with 300, CASEINCOMPLETE for the missing mode 3 item, and LATCH for duty keeping its value in mode 3.

  3. What does -Wno-fatal do to a Verilator lint run?

    It keeps the messages but exits with status 0, so a job that reads only the exit status passes a file full of warnings. A lint gate never passes it.

  4. Which bug did only slang report on the fan controller?

    Signed temp_c compared with unsigned start_c. One unsigned operand makes the comparison unsigned, so -5 °C counted as 251, above 30, and the fan started.

  5. How do slang and svlint decide what to check?

    slang's warnings come in groups, with -Wextra adding width and latch checks and single warnings named, such as -Wsign-compare. svlint checks the rules a team switches on in its .svlint.toml; every rule is off until enabled.

  6. What is wrong with -Wno-WIDTHTRUNC on the lint command line?

    It waives the warning on every line of every file, those written later included: it let a 5-bit result into a 4-bit duty and the override turned the fan off at 100 %. Waive one line, or a supplied file in a control file.

  7. How is a narrow Verilator waiver written?

    /* verilator lint_off WIDTHTRUNC */ before the line and /* verilator lint_on WIDTHTRUNC */ after it, with the reason beside it. A supplied file you may not edit gets a control-file line, lint_off -rule WIDTHTRUNC with its -file and -lines options.

  8. Why does the lint gate pass -Werror to slang?

    slang fails only on errors, so without -Werror its warnings print and the gate still passes; -Werror treats every warning as an error.

Key takeaways

  • A passing directed test checks only the inputs it drives; lint reads every line, and on fan_ctrl.sv it found four bugs that the test’s twelve checks missed.
  • Verilator reports lint warnings by default and style warnings with -Wall, and it exits with status 1 on any warning unless -Wno-fatal hides them from scripts.
  • slang applies the standard’s rules, such as the unsigned comparison that one unsigned operand makes, and svlint applies the rules a team switches on; each finds problems the others miss.
  • Fix a finding in the code where you can, and waive it only for one rule at one place with its reason, never with a -Wno flag for the whole design.
  • A lint gate runs every linter on every change and lets the change merge only when all of them end with status 0.

Terms in this lesson

continuous integration (CI)
A service that builds and checks every change pushed to a shared repository, such as a pull request, so that a change that fails a check is stopped before it merges.
control file (Verilator)
A file that begins with `verilator_config and holds commands such as lint_off -rule WIDTHTRUNC -file "*x.sv" -lines 10, so that a warning in a file you may not edit is waived without changing the file.
exit status
The number a program returns when it ends: 0 for success, any other value for failure. Scripts and continuous integration jobs read it to decide whether to go on.
style warning (Verilator)
A Verilator warning about a way of writing code that tends to cause mistakes later, such as BLKSEQ for a blocking = in a clocked block. Style warnings are off by default and switched on with -Wall.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.