sv-skill GitHub
Claude Code skill · svstyle CLI · 82 rules

SystemVerilog that reads like one engineer wrote it.

A house style for synthesizable RTL, a Claude Code skill that writes in it, and svstyle, the checker that enforces it through Verible, Verilator and Yosys. The rules, this page and the skill are generated from one file, so they never disagree.

82rules
59MUST · fail the check
53checked by a tool
15fixed automatically
fig. 1 · skeleton.sv posedge clk_i · async rst_ni
clk_irst_nivalid_idata_ivalid_odata_oA03F7C12E5A03F7C12E5A0 captured → data_o next cycle
The template every module starts from: valid is control and is reset; the payload is not, so data_o is X until the first valid input lands (RST-004).
§01

Quick start

Four steps from nothing to a module that passes lint, synthesis and its own testbench.

Install the skill

As a Claude Code plugin, from inside Claude Code:

claude code
/plugin marketplace add leandrotozzi/sv-skill
/plugin install systemverilog-rtl@sv-skill

Without the plugin system, copy or symlink skill/systemverilog-rtl into ~/.claude/skills/. The plugin also adds a hook that runs svstyle check --no-external after every .sv edit and tells Claude what it found. It never blocks.

Install the checker

The CLI the skill runs. Then the external tools, which are used when present.

shell
uv tool install svstyle        # or: pipx install svstyle
uvx svstyle check rtl/         # or run it once without installing

Verible v0.0-3946-g851d3ff4 (pinned) for formatting and lint, Verilator 5, and Yosys. The OSS CAD Suite ships Verilator, Yosys and the yosys-slang plugin in one download.

Configure the project (optional)

One .svstyle.toml at the repository root covers the tree.

.svstyle.toml
company = "ACME Corp"   # fills <Company> in every header
synth   = "yosys"       # or "yosys-slang"; checks synthesis on every run

Ask for hardware

No need to name the skill: any SystemVerilog request loads it.

  • Design a 4-way round-robin arbiter with a ready/valid output.
  • Add a 2-flop synchroniser for the busy flag into the 25 MHz domain.
  • Review rtl/fifo.sv against the house style.
  • Bring this Verilog-2001 UART into the house style without changing behaviour.
  • Why does svstyle flag DOC-004 on this line?
§02

How the skill works

What Claude does between your prompt and the finished RTL. Each step exists because skipping it cost a round of check, edit, check in the evals.

  1. 01

    Read the rules

    references/rules.md: one line per rule, short on purpose. Then the synthesis subset for the configured tool, and the multi-clock guide if the design needs it.

    • synth-yosys.md when synth is yosys or unset
    • synth-yosys-slang.md for yosys-slang
    • multi-clock.md + rules-cdc.md with more than one clock or an asynchronous input
  2. 02

    Start from a template

    Never a blank file: each template in assets/ already passes svstyle check, so header, port order, reset style and comment layout come for free. Module and file are renamed together.

    • skeleton.sv: any module
    • fsm.sv + fsm_pkg.sv: a state machine
    • sync_ff.sv: a synchroniser
    • skeleton_tb.sv: the testbench
    • assert_macros.svh: `ASSERT, `ASSERT_KNOWN
  3. 03

    Write the comments as it goes

    The fixer can only insert // TODO: describe, which keeps the file passing and flags a gap. Real comments are written with the code: header description, every port and declaration, a line above every block and instance, every FSM arm.

    Widths match on every assignment, no keyword used as a name, no temporary with bits never read, no assertion the type already guarantees.
  4. 04

    svstyle fix

    Verible formats the file; svstyle inserts what is missing (header, `default_nettype none / `resetall, labels) and modernises legacy code only where the syntax tree proves the meaning is unchanged.

    reg/wire → logic, always @* → always_comb, always @(posedge) → always_ff, generate removed, functions made automatic.
  5. 05

    svstyle check --synth

    One report from four tools, every finding as file:line:col: RULE-ID message. [fix available] means fix handles it; the rest is the author's.

    Every file of the design in one call, so Verilator can elaborate the hierarchy. -I DIR for `includes elsewhere.
  6. 06

    Simulate

    A clean check means well formed, not correct. A self-checking Verilator testbench in tb/<name>_tb.sv compares the outputs, every cycle, against a model written from the spec, never from the RTL.

    Skipped for small blocks: one clock, no memory or protocol, simple arithmetic, no fairness or ordering promise, under ~40 lines of logic. On a FAIL: decide from the spec which side is wrong, fix, rerun once.
  7. 07

    Walk the judgement list

    About a third of the rules need judgement. Before calling it done the skill checks them against the code: reset scope, FSM shape, unique case + default, parameter checks, assertions, naming, structure.

    A genuine departure is waived where it happens, with the reason: // svstyle: waive RST-001 DSP blocks need a synchronous reset.

Done means

  • svstyle check --synth exits 0 on every file of the design. SHOULD findings may remain, each one a deliberate choice that is reported.
  • No // TODO: describe is left anywhere in the RTL.
  • The testbench printed PASS and no `ASSERT fired, or the block was small enough to skip it, and the skill says so.
  • The judgement list was walked; every departure carries a DOC-009 waiver with its reason.
  • The summary says what ran, what it reported, which SHOULDs stayed and why, and what the testbench covered (directed cases, random cycles, seed) and did not.

When the interface is given

A spec, a benchmark, vendor IP or existing code may fix the module name, port names or the reset style (clk, reset, a synchronous active-high reset). Then the skill keeps them exactly as given: the house style is for what it chooses, not for a contract it was handed. Everything inside still follows the style, and what the interface forces is waived with the reason, which doubles as the port comment.

a port list fixed by a spec
input  logic clk,    // svstyle: waive NAM-002 fixed interface; system clock
input  logic reset,  // svstyle: waive NAM-002 fixed interface; sync reset, active-high

Reviewing, or bringing legacy RTL into the style, follows existing-code.md: fix modernises only what it can prove unchanged, and --baseline lets CI fail only on new findings.

What the skill reads, and when

Only SKILL.md and rules.md are always loaded; everything else is read on demand, to keep the context small.

referenceloaded whenholds
rules.mdalways, before any codeOne line per rule, every category but CDC.
synth-yosys.mdsynth = "yosys" or no keyThe SystemVerilog subset Yosys reads, and the constructs it synthesises wrong silently.
synth-yosys-slang.mdsynth = "yosys-slang"The much shorter list left with the slang frontend.
multi-clock.md, rules-cdc.mdmore than one clock, an async input, or a reset crossingSynchronisers, reset synchronisers, and the patterns for multi-bit crossings.
existing-code.mdreviewing RTL or bringing legacy code into the styleBaselines, what fix may rewrite, how to report findings.
testbench-verilator.mdthe Simulate stepWhere the testbench lives, what it may use, what it must check, the build command.
rules-rationale.mda rule is questioned or a finding is unclearEvery rule with its rationale and examples; searched by heading, never loaded whole.
tooling.mdsvstyle missing, exit codes, JSON, waiversThe CLI reference, and which tool enforces which rule.
§03

The house style

lowRISC lineage: _i/_o, clk_i, rst_ni, _q/_d, CamelCase parameters, 100 columns; four-space indent is ours. Six ideas carry the rules.

DOC

Comments are part of the code

A header with a real description; a trailing comment on every port and declaration; a line above every block, instance and FSM arm. Checked and inserted by svstyle; no other open-source tool does it.

NAM

Names carry the hardware

Direction (_i/_o), register (_q/_d), polarity (_n) and type (_e/_t) are in the name, so a reader never scrolls up to a declaration.

LNG

One way to write each thing

logic everywhere; always_ff with <=, always_comb with = and a default for every output; every case has a default; literals state their width.

RST

Reset control, not data

Asynchronous active-low rst_ni, through a synchroniser per clock domain. State, counters and valids are reset; the datapath they qualify is not.

FMT

The formatter owns whitespace

Four spaces, 100 columns, tabular port lists and connections: Verible's output, not anyone's taste. Nothing about layout is ever discussed in review.

SYN

Synthesis is the contract

Code in the subset the synthesis tool reads; no full_case, no combinational loops, no initial, no recursion. What simulates must be what is built.

Severity

MUST · 59

A violation fails svstyle check (exit 1).

SHOULD · 23

Reported, does not fail. Leaving one is a choice the author can explain.

Any finding, from any tool, is waived with one comment that names the rule and says why: // svstyle: waive DOC-004 <reason>. On its own line it covers the next line; trailing a line, that line. A waiver without a reason is ignored (DOC-009).

Naming at a glance

patternmeansexamplerule
_i _o _ioport direction, always lastdata_i, valid_o, pad_ioNAM-002
_q / _dflop output / its computed next valuecount_q, count_dNAM-004
_nactive low, stacked firstrst_ni, cs_no, cs_nqNAM-003
clk_i, rst_niclock first, then async active-low resetclk_i, rst_niPRT-002RST-001
_e / _tenum type / other typedefstate_e, pkt_tNAM-005
UpperCamelCaseparameters and localparams derived from themWidth, NumPortsNAM-006
ALL_CAPStrue constants and macrosAXI_RESP_OKAY, `ASSERTNAM-006NAM-008
gen_generate block labelsgen_port, gen_width_checkGEN-001
<name>_pkga package, in <name>_pkg.svfsm_pkgSTR-001
lower_snake_caseeverything else, in whole wordsrd_ptr_q, tx_busyNAM-001NAM-009

Anatomy of a module

The template every new module starts from. Hover a note to find its line.

assets/skeleton.sv — passes svstyle check as shipped
// Copyright (c) 2026 <Company>1// All rights reserved//// Description://   Canonical module skeleton: registered valid/data stage with an enable.//   Copy it, rename the module and file together, and replace the logic. `default_nettype none2 `include "assert_macros.svh" module skeleton #(    parameter int unsigned Width = 8  // Payload width in bits; at least 13) (    input  logic             clk_i,    // System clock4    input  logic             rst_ni,   // Asynchronous reset, active-low5    input  logic             valid_i,  // Payload on data_i is valid this cycle6    input  logic [Width-1:0] data_i,   // Payload in    output logic             valid_o,  // Payload on data_o is valid this cycle    output logic [Width-1:0] data_o    // Payload out, one cycle after data_i);    // Width must be positive: a zero-width payload has no meaning.    if (Width < 1) begin : gen_width_check7        $error("Width must be at least 1");    end     logic valid_q, valid_d;  // Output valid register8    logic [Width-1:0] data_q, data_d;  // Output payload register     // Load the payload on a valid input; hold it otherwise.    always_comb begin9        valid_d = valid_i;        data_d  = data_q;        if (valid_i) begin            data_d = data_i;        end    end     // Valid is control and is reset; the payload is qualified by it and is not.    always_ff @(posedge clk_i or negedge rst_ni) begin10        if (!rst_ni) begin            valid_q <= 1'b0;        end else begin            valid_q <= valid_d;        end    end     // Payload register, no reset (RST-004).11    always_ff @(posedge clk_i) begin        data_q <= data_d;    end     assign valid_o = valid_q;    assign data_o  = data_q;     // Valid is never X once out of reset; the unreset payload only while valid (VER-002).    `ASSERT_KNOWN(ValidKnown_A, valid_o)12    `ASSERT(DataKnownIfValid_A, valid_o |-> !$isunknown(data_o))endmodule `resetall
  1. 1

    Every file opens with the copyright block and a Description: that names the non-obvious constraints. svstyle fix fills <Company> from .svstyle.toml.

  2. 2

    A typo in a net name is an error, not a new implicit wire. The file ends with `resetall.

  3. 3

    Typed parameter, UpperCamelCase, with a trailing comment that says what it means and its legal range.

  4. 4

    ANSI ports, one per line: clock first, then reset, then the rest. Every port ends in its direction: _i, _o, _io.

  5. 5

    Asynchronous, active-low reset. Suffixes stack in a fixed order: _n before _i.

  6. 6

    A trailing comment on every port: meaning and validity, not type or direction. Verible aligns the columns.

  7. 7

    An illegal parameter fails at elaboration, in a labelled gen_ block.

  8. 8

    logic everywhere. A flop is _q; its computed next value _d. Every declaration carries a comment.

  9. 9

    A comment line above every block. always_comb with blocking assignments, and a default for every output so no latch is inferred.

  10. 10

    always_ff with nonblocking assignments, async reset in the sensitivity list, begin/end always written.

  11. 11

    Only control state is reset. The payload is qualified by valid, so a reset on it would only cost area and routing.

  12. 12

    Outputs are checked for X through the standard macros, which compile to nothing under synthesis.

§04

All 82 rules

Each rule, why it exists, what good and bad look like, and which tool enforces it. Filter by category, severity or tool; link to any rule by its ID, e.g. #RST-001.

82 / 82 shown · click an ID to copy it
DOCComments and file header
MUSTAUTOFIX

Every file opens with the standard header

Every .sv and .svh file begins with the copyright line, the all-rights-reserved line, and a Description: block that says what the file does. A file that opens with the project's own licence header keeps it, followed by the Description: block.

enforced by svstyle · header

Why, and examples

The description is the only part of a file that survives being read out of context -- in a diff, a search result, or a model's context window. Copyright and licence state must be mechanical, not remembered, because a file that ships without it is a legal problem discovered late.

good
// Copyright (c) 2026 ACME Corp
// All rights reserved
//
// Description:
//   Synchronous FIFO with first-word-fall-through. Depth must be a power of
//   two. Full and empty are registered.
bad
// fifo.sv
module fifo #(

Sources: house style, lowRISC §Basic Template (shown as a template, not stated as a rule)

SHOULD

The header carries a real description

The Description: block says what the module does and names its non-obvious constraints; it is not the module name restated.

enforced by svstyle · header_description

Why, and examples

A description that repeats the identifier adds a line and no information. The constraints -- power-of-two depth, one-hot input assumption, latency -- are what a reader cannot recover from the port list.

good
// Description:
//   Gray-code pointer synchroniser. Source and destination clocks are
//   asynchronous; the pointer must change by at most one per source clock.
bad
// Description:
//   Gray counter module.

Sources: house style

MUSTAUTOFIX

Every port carries a trailing comment

Each port declaration ends with a // comment describing the signal.

enforced by svstyle · port_comment

Why, and examples

The port list is the module's contract, and it is the one part of a module that other engineers and models read without opening the body. Direction and type are in the declaration; what the signal means, when it is valid and what it is relative to are not.

good
input  logic        clk_i,    // System clock
input  logic        rst_ni,   // Asynchronous reset, active-low
input  logic [7:0]  data_i,   // Payload byte, valid while valid_i is high
output logic        ready_o   // Sink can accept a byte this cycle
bad
input  logic        clk_i,
input  logic        rst_ni,
input  logic [7:0]  data_i,
output logic        ready_o

Sources: house style

MUSTAUTOFIX

Every declaration carries a trailing comment

Each parameter, localparam, signal and net declaration statement ends with a // comment. A declaration spread over several lines, or one whose comment would not fit trailing within 100 columns, may carry it on the line directly above instead. Enumeration members are exempt.

enforced by svstyle · decl_comment

Why, and examples

A declaration is where a signal's purpose can be stated once instead of re-derived at every use. Enum members are exempt because their meaning belongs on the FSM case arm that acts on them, which is where lowRISC puts it -- commenting both places guarantees the two drift apart.

good
localparam int unsigned DepthW = $clog2(Depth);  // Bits needed to index the RAM
logic [DepthW-1:0] wptr_q, wptr_d;               // Write pointer, binary
bad
localparam int unsigned DepthW = $clog2(Depth);
logic [DepthW-1:0] wptr_q, wptr_d;

Sources: house style

MUSTAUTOFIX

Every procedural block is introduced by a comment

A one-line // comment sits on the line above every always_ff, always_comb and always_latch block, saying what the block computes.

enforced by svstyle · block_comment

Why, and examples

Orientation without structure: the comment scrolls with the code and folds with it, so a reader skimming a collapsed module still sees what each block is for. Framed banner comments would do the same job but break native editor folding and rot when code moves.

good
// Advance the write pointer on an accepted write.
always_ff @(posedge clk_i or negedge rst_ni) begin
bad
always_ff @(posedge clk_i or negedge rst_ni) begin

Sources: house style

MUSTAUTOFIX

Every instance is introduced by a comment

A one-line // comment sits on the line above every module instantiation, saying what the instance is for in this design.

enforced by svstyle · instance_comment

Why, and examples

The instance name says which module; the comment says why this design needs it. A reader tracing a signal through a hierarchy needs the second, and it is the thing the code cannot state.

good
// Two-flop synchroniser for the request from the slow domain.
prim_flop_2sync #(.Width(1)) u_req_sync (
bad
prim_flop_2sync #(.Width(1)) u_req_sync (

Sources: house style

MUST

Comment placement follows the code it describes

A comment on its own line describes the code that follows; a comment on a line with code describes that line. Use //, not /* */.

enforced by review only

Why, and examples

Quoting lowRISC: "A comment on its own line describes the code that follows. A comment on a line with code describes that line of code." Consistent placement is what lets a tool insert and a reader trust trailing comments at all.

good
// Hold the request until the far side acknowledges.
assign req_d = req_q & ~ack_i;  // Cleared by the synchronised ack
bad
assign req_d = req_q & ~ack_i;
// Hold the request until the far side acknowledges.

Sources: lowRISC §Comments

MUST

Every FSM state arm is introduced by a comment

Each state alternative inside an FSM case statement is preceded by a comment describing that state's function; those descriptions do not also live on the enum declaration.

enforced by review only

Why, and examples

Quoting lowRISC: "each state alternative should be preceded with a comment that describes the function of that state within the state machine". Keeping the description on the arm and not on the enum member means there is one copy to keep true.

good
// IDLE: wait for a request; nothing is in flight.
IDLE: begin
    if (req_i) state_d = BUSY;
end
bad
typedef enum logic [1:0] {
    IDLE,  // wait for a request
    BUSY   // transfer in flight
} state_e;

Sources: lowRISC §Finite State Machines

MUST

Every deviation from this style is justified in a comment

Code that deliberately departs from a rule in this guide carries a brief comment saying why, plus a lint waiver pragma where the tool supports one.

enforced by review only

Why, and examples

Quoting lowRISC: "It is always okay to deviate from the style guide by necessity, as long as that necessity is clearly justified by a brief comment". An unexplained deviation is indistinguishable from a mistake, so the next reader either breaks it or copies it.

good
wire tri_bus;  // Must be wire: connects to an inout port (style exception)
bad
wire tri_bus;

Sources: lowRISC §Style Guide Exceptions

SHOULDAUTOFIX

Long conditional blocks close with a matching label

A endif far from its ifdef carries a comment naming the condition it closes, and a named begin closes as end : name.

enforced by verible lint · endif-comment mismatched-labels

Why, and examples

The opening condition has scrolled off screen by the time the reader reaches the close; without the label, matching them is a manual count. Verible checks both mechanically.

good
`ifdef SYNTHESIS
    // ...
`endif  // SYNTHESIS
bad
`ifdef SYNTHESIS
    // ...
`endif

Sources: lowRISC §Comments, Verible

STRFile structure
MUST

One module per file, named after the module

A source file contains exactly one module, package or interface, and the file's basename is that design unit's name.

enforced by verible lint · module-filename package-filename one-module-per-file — also verilator · DECLFILENAME

Why, and examples

Every build system, waveform viewer and synthesis report addresses design units by name and finds them by filename. When the two disagree the mapping becomes a lookup only the author knows.

good
// fifo_sync.sv
module fifo_sync #(
bad
// fifo.sv
module fifo_sync #(

Sources: lowRISC §Files, Verilator DECLFILENAME

MUST

SystemVerilog files use .sv, headers use .svh

SystemVerilog source files are named .sv; files intended to be included are named .svh`.

enforced by svstyle · file_extension

Why, and examples

The extension is how every tool decides whether to compile a file as a translation unit or treat it as text to be pulled into one. A header compiled standalone fails in ways that read as syntax errors far from the cause.

good
prim_assert.svh
bad
prim_assert.sv  // but only ever `include`d

Sources: lowRISC §Files

SHOULD

Declare signals close to their first use

Signals, types, enums and localparams are declared close to where they are first used, grouped with the logic that drives them, rather than hoisted into a block at the top of the module.

enforced by review only

Why, and examples

Quoting lowRISC: "This makes it easier for the reader to find the declaration and see the signal type." Measured practice agrees -- 74.9% of OpenTitan declarations follow the first statement of the module. A hoisted block puts the type a screen away from every use.

good
// Write pointer, binary, wraps at Depth.
logic [PtrW-1:0] wptr_q, wptr_d;
always_ff @(posedge clk_i or negedge rst_ni) begin
bad
logic [PtrW-1:0] wptr_q, wptr_d;
logic [PtrW-1:0] rptr_q, rptr_d;
logic            full, empty;
// ... 200 lines ...

Sources: lowRISC §Declarations, OpenTitan practice (74.9% near-use)

MUST

Packages do not depend on each other cyclically

Package dependencies form a directed acyclic graph; two packages never import each other.

enforced by review only

Why, and examples

SystemVerilog has no forward declaration for package contents, so a cycle has no legal compilation order. Tools that appear to accept one are relying on file order, which changes silently when the build system does.

good
package top_pkg;
    import common_pkg::*;  // common_pkg imports nothing from top_pkg
bad
package a_pkg; import b_pkg::*; endpackage
package b_pkg; import a_pkg::*; endpackage

Sources: lowRISC §Packages

SHOULD

Wildcard imports are local and justified

import pkg::*; is used only for a package belonging to the same IP as the importing module, and the import sits in the module header rather than at file scope.

enforced by svstyle · import_scope

Why, and examples

A file-scope wildcard import leaks every name in the package into every design unit in the file and into anything that includes it, so a later addition to the package can capture an identifier that already resolved elsewhere. Scoping it to the module bounds the damage to one unit.

good
module hmac_core
    import hmac_pkg::*;
#(
bad
import hmac_pkg::*;
module hmac_core #(

Sources: lowRISC §Packages

PRTPorts and connections
MUST

Ports are declared in ANSI style, one per line

Module ports use the Verilog-2001 combined port-and-direction style, one port per line, with the closing ); on its own line.

enforced by svstyle · ansi_ports

Why, and examples

The non-ANSI style splits each port's direction, type and order across three places, so adding a port means three consistent edits and the compiler catches only some mistakes. One port per line is also what makes a trailing comment per port possible at all.

good
module fifo_sync #(
    parameter int unsigned Depth = 8  // Entries; must be a power of two
) (
    input  logic clk_i,   // System clock
    input  logic rst_ni   // Asynchronous reset, active-low
);
bad
module fifo_sync (clk, rst_n);
    input clk;
    input rst_n;

Sources: lowRISC §Module Declaration

MUST

Clocks first, then resets, then the rest

The port list opens with a clock, then its resets, then the remaining ports grouped by interface. A module with several clocks repeats the pattern per domain: each clock opens its own group with its resets right after it.

enforced by svstyle · port_order

Why, and examples

A fixed prefix means the reader checks the clocking and reset scheme of an unfamiliar module by looking at one place, and an instantiation's first two lines are comparable across every module in the tree.

good
input  logic clk_i,    // System clock
input  logic rst_ni,   // Asynchronous reset, active-low
input  logic req_i,    // Transfer request
bad
input  logic req_i,    // Transfer request
input  logic clk_i,    // System clock
input  logic rst_ni,   // Asynchronous reset, active-low

Sources: lowRISC §Module Declaration

MUST

Instance ports are connected by name, never by position or .*

Every instantiation connects ports by name. Positional connection and .* are not used in synthesizable RTL.

enforced by svstyle · named_ports

Why, and examples

A positional list breaks silently when a port is inserted, and .* makes the connection depend on names matching in two files that no longer have to agree in writing -- a renamed signal connects to nothing and reports nothing. Measured practice agrees: .* appears in 28 OpenTitan files and every one of them is under dv/, fpv/, cov/ or sva/, never in synthesizable RTL.

good
fifo_sync u_fifo (
    .clk_i  (clk_i),
    .rst_ni (rst_ni),
    .data_i (data_i)
);
bad
fifo_sync u_fifo (.*);
fifo_sync u_fifo (clk_i, rst_ni, data_i);

Sources: lowRISC §Module Instantiation, BSG §prohibitions, OpenTitan practice (0 uses in RTL)

MUST

Every port of an instance is connected explicitly

Every declared port appears in every instantiation, in declaration order; unused outputs are written as explicit no-connects and unused inputs are tied off explicitly.

enforced by verilator · PINMISSING

Why, and examples

An omitted port is indistinguishable from an oversight, and an unconnected input floats to z or a tool-dependent default rather than failing. Writing the tie-off states the intent where the next reader will look for it.

good
.status_o (),          // Unused: this instance never reports status
.debug_i  (1'b0),      // Tied off: debug hooks are not built in this config
bad
// status_o and debug_i simply absent from the instantiation

Sources: lowRISC §Module Instantiation

MUST

Parameters are overridden by name, never by defparam

Instance parameters are specified by name in the #(...) list. defparam is not used.

enforced by verible lint · forbid-defparam — also verilator · DEFPARAM — also svstyle · named_params

Why, and examples

defparam applies from a distance, so the effective parameter value of an instance cannot be read from the instantiation; its elaboration order is also weakly specified and it is deprecated in IEEE 1800. Named overrides keep the value where the instance is.

good
fifo_sync #(
    .Depth (16)
) u_fifo (
bad
fifo_sync u_fifo ( ... );
defparam u_fifo.Depth = 16;

Sources: lowRISC §Module Instantiation, Verilator DEFPARAM

SHOULD

Interfaces are not used as ports of reusable RTL

Synthesizable, reusable modules take individual signals or packed structs as ports, not SystemVerilog interfaces.

enforced by review only

Why, and examples

Interface-typed ports have uneven synthesis support across Vivado, Quartus, Design Compiler, Genus and Yosys, and they hide the module's real fan-in and fan-out from lint and CDC tools. Measured practice is unanimous: zero interface-typed ports in synthesizable RTL across OpenTitan, Ibex and PULP.

good
input  tlul_pkg::tl_h2d_t tl_i,   // TileLink host-to-device, packed struct
output tlul_pkg::tl_d2h_t tl_o    // TileLink device-to-host
bad
tlul_if.device tl   // interface port in a reusable module

Sources: PULP practice, OpenTitan/Ibex/PULP measurement (0 uses)

SHOULD

Handshake suffixes state the protocol contract

A ready/valid interface names its backpressure signal for the protocol it implements: _ready_and_o when neither signal may depend combinationally on the other, _ready_then_o when valid may depend on ready, _yumi_o when the accept may depend on valid. A bare _ready_o does not say which.

enforced by review only

Why, and examples

The difference between these protocols is which signal is allowed to depend combinationally on which, and getting it wrong creates a combinational loop only at the point where two modules are finally connected -- often across a hierarchy boundary, long after both passed lint alone. The name is the only place the contract can live, because no linter can check it. Only ready_and and credit are safe at a top level where both ends are unknown.

good
output logic data_ready_and_o,  // r&v: must not depend on data_valid_i
input  logic data_valid_i       // r&v: must not depend on data_ready_and_o
bad
output logic data_ready_o   // Which protocol? The consumer cannot tell.

Sources: BSG SystemVerilog coding standard §handshakes, Taylor et al., DAC 2018

NAMNaming
MUST

Identifiers are lower_snake_case

Modules, packages, interfaces, instances, signals, ports, variables, functions, tasks and named blocks are named in lower_snake_case.

enforced by verible lint · signal-name-style interface-name-style struct-union-name-style

Why, and examples

A single case convention means a reader never has to decide whether a difference in case is meaningful. The exceptions below -- parameters, constants, macros, types -- are deliberate: each marks a category of identifier whose binding time differs from a signal's.

good
logic [7:0] write_data_q;
bad
logic [7:0] writeData_q;

Sources: lowRISC §Naming Conventions

MUST

Ports carry a direction suffix

Module inputs end in _i, outputs in _o, and bidirectionals in _io.

enforced by verible lint · port-name-suffix

Why, and examples

Direction is declared once in the port list but read at every use inside the module and at every connection outside it. The suffix carries it to both places, so a signal driven in the wrong direction is visible where the mistake is written rather than where it is compiled.

good
input  logic       req_i,    // Transfer request
output logic [7:0] data_o    // Payload
bad
input  logic       req,
output logic [7:0] data

Sources: lowRISC §Suffixes

MUST

Suffixes stack in a fixed order

Multiple suffixes are concatenated without extra underscores, with _n first and _i/_o/_io last: rst_ni, data_qi, valid_no.

enforced by svstyle · suffix_order

Why, and examples

Without a fixed order the same signal is spelled rst_in in one module and rst_ni in the next, and the two no longer sort, grep or connect as the same name. Active-low-before-direction also keeps the direction as the true final token, which is what the linter's suffix regex anchors on.

good
input logic rst_ni,  // Asynchronous reset, active-low
bad
input logic rst_in,  // same signal, different spelling

Sources: lowRISC §Suffixes

MUST

Flop pairs are _q and _d

A flip-flop is named <name>_q. When combinational logic computes its next value, that value is named <name>_d and declared next to it; a flop loaded straight from a port, another flop or a memory needs no _d. An output port driven straight from a flop keeps its _o.

enforced by verible lint · dff-name-style

Why, and examples

The pair makes a register visible as a register at every use, so reading a value one cycle late -- the most common RTL mistake -- becomes a spelling difference the reader can see rather than a timing property they have to reconstruct.

good
logic [7:0] count_q, count_d;  // Byte counter
always_comb count_d = count_q + 8'd1;
bad
logic [7:0] count, count_next;

Sources: lowRISC §Suffixes

MUST

Type names end in _e or _t

Enumeration types are named lower_snake_case_e, other typedefs lower_snake_case_t, and every enum and struct used as a type is given a typedef name.

enforced by verible lint · enum-name-style typedef-enums typedef-structs-unions

Why, and examples

A declaration reads state_e state_q; -- type and variable are distinguishable without looking either up. Anonymous enums and structs cannot be named in a port list, a function signature or a package, so they force a copy of the definition at each use.

good
typedef enum logic [1:0] { IDLE, BUSY, DONE } state_e;  // Transfer states
state_e state_q, state_d;                               // Current/next state
bad
enum logic [1:0] { IDLE, BUSY, DONE } state_q, state_d;

Sources: lowRISC §Naming Conventions

SHOULD

Parameters are UpperCamelCase, true constants ALL_CAPS

Tunable parameters and the localparams derived from them are named in UpperCamelCase; values that are immutable facts rather than tuning knobs are named in ALL_CAPS.

enforced by verible lint · parameter-name-style parameter-type-name-style

Why, and examples

The case says whether overriding the value at instantiation is expected. Both spellings are accepted by the linter's CamelCase check, which matches lowRISC's own split rather than contradicting it. Measured practice: 99.8% UpperCamel across lowRISC code.

good
parameter  int unsigned Depth  = 8;             // Entries; override per instance
localparam int unsigned PtrW   = $clog2(Depth); // Derived, not tunable
localparam logic [7:0]  SOF_BYTE = 8'h7E;       // Protocol constant, immutable
bad
parameter int unsigned depth = 8;

Sources: lowRISC §Naming Conventions, lowRISC practice (99.8% UpperCamel)

SHOULD

Parameter names state the positive sense

A boolean parameter is named for the condition being true (EnableLogging), not negated (DisableLogging).

enforced by verible lint · positive-meaning-parameter-name

Why, and examples

A negated name turns every use into a double negative -- if (!DisableLogging) -- which is where sign mistakes hide. Defaults also read wrong: DisableLogging = 0 requires two steps to mean "logging is on".

good
parameter bit EnableAssertions = 1;  // Compile the SVA blocks
bad
parameter bit DisableAssertions = 0;

Sources: Verible, house style

SHOULD

Macros are ALL_CAPS and prefixed by their group

define macros are named in ALL_CAPS. A globally visible macro is prefixed with its group name and a double underscore; a file-local macro is prefixed with a single underscore and undefed after use.

enforced by verible lint · macro-name-style macro-string-concatenation

Why, and examples

Macros ignore every scope in the language, so the name is the only namespace they have. Undefining a file-local macro keeps it from leaking into whatever file includes this one next, where a collision would silently take the wrong definition.

good
`define PRIM_ASSERT__VALID(x) ...
`define _local_helper(x) ...
`undef _local_helper
bad
`define VALID(x) ...

Sources: lowRISC §Naming Conventions

SHOULD

Names describe purpose in whole words

Signal names say what the signal is for, using whole words; signals that work together share a prefix, and a signal in a non-default clock domain is prefixed with that domain's identifier.

enforced by review only

Why, and examples

The clock-domain prefix is the load-bearing half: it makes a crossing visible at the point of assignment, so assign vdata = udata; reads as a bug on the line where it is written rather than at CDC-analysis time. Whole words matter because the reader of an abbreviation is usually not its author.

good
logic [7:0] uart_rx_data_q;   // uClk domain
logic [7:0] vsync_rx_data_q;  // vClk domain -- crossing is visible here
bad
logic [7:0] urxd_q;
logic [7:0] vrxd_q;

Sources: lowRISC §Use descriptive names, Cummings SNUG-2001 §clock domain naming

MUST

Keywords are never used as names

No identifier reuses a Verilog or SystemVerilog keyword, including keywords added after Verilog-2005.

enforced by svstyle · keyword_name

Why, and examples

SystemVerilog reserved many words that were legal Verilog-2001 identifiers, so a name that compiles today can break when a tool moves to a newer language level -- as a syntax error far from the declaration.

good
logic packet_type;
bad
logic type;  // `type` is a SystemVerilog keyword

Sources: lowRISC §Naming Conventions

FMTFormatting and layout
MUSTAUTOFIX

The formatter owns whitespace

Indentation is four spaces, never tabs; lines carry no trailing whitespace; the file ends with exactly one newline; lines are at most 100 columns.

enforced by verible lint · no-tabs no-trailing-spaces posix-eof line-length forbid-line-continuations — also svstyle · comment_line_length

Why, and examples

Bundled into one rule because none of them is a judgement call -- the formatter produces all four and a reviewer never has to discuss any of them. Four spaces match Python and make nesting depth readable at a glance; 100 columns follows the RISC-V open-source ecosystem this style descends from; 80 is too narrow once names carry direction and register suffixes.

good
always_comb begin
    data_d = data_i;
end
bad
always_comb begin
	data_d = data_i;   
end

Sources: lowRISC §Basics (column limit), house style (four-space indent), Verible defaults

MUSTAUTOFIX

Port lists, parameters and named connections are tabular

Port declarations, formal parameters, named port connections, named parameter overrides, module-level net declarations, struct members and enum assignments are aligned into columns. Loose assignment statements and case items are not.

enforced by verible format

Why, and examples

These constructs are read as tables -- a reader scans one column to answer "what is the width of every port" -- and alignment is what makes the column exist. Loose assignments are excluded deliberately: aligning them makes every future edit rewrite its neighbours' whitespace, which is the cost that keeps Black from aligning anything. lowRISC requires alignment only for instantiation port expressions; the rest is ours, and costs nothing because the formatter does it.

good
input  logic        clk_i,    // System clock
input  logic [31:0] addr_i,   // Byte address
output logic        valid_o   // Response valid
bad
input logic clk_i, // System clock
input logic [31:0] addr_i, // Byte address
output logic valid_o // Response valid

Sources: lowRISC §Tabular Alignment, house style (the wider set)

MUSTAUTOFIX

begin and end are always written

Every if, else, for, while and always body is wrapped in begin/end, even a single statement. begin sits on the line of the construct that opens it and end else begin is written on one line.

enforced by verible lint · explicit-begin module-begin-block

Why, and examples

A single-statement body that grows a second statement is a silent bug, and it is exactly the edit a model or a hurried human makes. This is stricter than lowRISC, which permits omitting begin/end when the whole statement fits on one line. Note the cost: neither the formatter nor the linter can insert begin/end, so on existing code every violation is a manual edit.

good
if (clear_i) begin
    count_d = '0;
end else begin
    count_d = count_q + 1'b1;
end
bad
if (clear_i) count_d = '0;
else count_d = count_q + 1'b1;

Sources: house style (stricter than lowRISC §begin/end)

MUST

No stray or duplicated semicolons

There are no empty statements from consecutive semicolons, and no semicolon after the end of a block or after a module or generate body.

enforced by verible lint · forbid-consecutive-null-statements suspicious-semicolon

Why, and examples

A stray semicolon after if (cond); silently makes the body empty and the following block unconditional -- it parses, simulates and synthesises, just not as written.

good
if (valid_i) begin
  data_d = data_i;
end
bad
if (valid_i);
begin
  data_d = data_i;
end

Sources: Verible

SHOULD

Mixed operators are parenthesised

An expression mixing operators of different precedence carries explicit parentheses around the intended grouping.

enforced by verible lint · suggest-parentheses

Why, and examples

SystemVerilog's precedence table places the bitwise and comparison operators in an order that surprises most readers coming from C, and the failure is a wrong-but-plausible value rather than an error.

good
assign hit = (tag_q == tag_i) && (valid_q | force_i);
bad
assign hit = tag_q == tag_i && valid_q | force_i;

Sources: Verible

LNGLanguage features
MUSTAUTOFIX

logic everywhere; wire only for a real tri-state

Signals are declared logic. wire appears only where a net is genuinely multiply driven, which in practice means a bidirectional pad, and carries a comment saying so.

enforced by svstyle · logic_only

Why, and examples

logic is four-state like reg and wire but carries a single-driver check the compiler enforces, so an accidental second driver is an error at elaboration instead of a resolved value that simulates as x and synthesises as a short.

good
logic [7:0] data_q;  // Registered payload
bad
reg [7:0] data_q;

Sources: lowRISC §Use logic for synthesis

MUSTAUTOFIX

Sequential logic is always_ff with nonblocking assignments

Clocked logic is written in always_ff and assigns only with <=.

enforced by verible lint · always-ff-non-blocking — also svstyle · always_ff_style

Why, and examples

Cummings SNUG-2000 guideline #1. Blocking assignments in a clocked block execute in the active region of the stratified event queue, so a flop reading another flop assigned earlier in the same block sees the new value instead of the old one -- the simulation shows one pipeline stage where the synthesised gates have two, and which one you get depends on statement order.

good
always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
        b_q <= '0;
    end else begin
        b_q <= a_q;   // Reads the OLD a_q, as the hardware does
    end
end
bad
always_ff @(posedge clk_i) begin
    a_q = data_i;
    b_q = a_q;      // Reads the NEW a_q: one flop in sim, two in gates
end

Sources: Cummings SNUG-2000 guideline #1, lowRISC §Sequential Logic

MUSTAUTOFIX

Combinational logic is always_comb with blocking assignments

Combinational logic is written in always_comb and assigns only with =. always @(*) and manual sensitivity lists are not used.

enforced by verible lint · always-comb always-comb-blocking — also svstyle · always_comb_style — also verilator · ALWCOMBORDER

Why, and examples

Cummings SNUG-2000 guideline #3, plus the sensitivity-list half: always_comb is checked by the tool for completeness and for having no blocking/nonblocking mix, where always @(*) is inferred and an incomplete manual list produces a latch in synthesis that simulation does not show. Nonblocking assignments inside a combinational block defer the update to the NBA region, so a value read later in the same block is stale.

good
always_comb begin
    sum = a_i + b_i;
    carry = sum[8];     // Sees the value just assigned
end
bad
always @(a_i) begin   // b_i missing: latch
    sum <= a_i + b_i;
end

Sources: Cummings SNUG-2000 guidelines #3, #10, lowRISC §Combinational Logic

MUST

Blocking and nonblocking assignments are never mixed in one block

A single always block uses either = or <= throughout, never both.

enforced by verilator · BLKSEQ COMBDLY

Why, and examples

Cummings SNUG-2000 guideline #5. A mixed block splits its own updates across the active and NBA regions, so the behaviour depends on the order the two kinds of statement happen to appear in -- and is not what either kind alone would give.

good
always_ff @(posedge clk_i) begin
    a_q <= a_d;
    b_q <= b_d;
end
bad
always_ff @(posedge clk_i) begin
    temp = a_d;
    b_q <= temp;
end

Sources: Cummings SNUG-2000 guideline #5

MUST

A signal is driven from exactly one block

No variable is assigned from more than one always block or continuous assignment.

enforced by verilator · MULTIDRIVEN — also yosys · multiple-drivers

Why, and examples

Cummings SNUG-2000 guideline #6. Two blocks assigning one variable race: the simulator may execute them in either order within a time step, and synthesis resolves the conflict as a short or an arbitrary winner. The failure is order-dependent, so it often appears only after an unrelated edit.

good
always_comb begin
    data_d = load_i ? data_i : data_q;
end
bad
always_comb data_d = data_i;
always_comb if (hold_i) data_d = data_q;

Sources: Cummings SNUG-2000 guideline #6

MUST

Every combinational output has a default assignment

A combinational block assigns a default value to every signal it drives, immediately after the block opens and before any conditional logic.

enforced by verilator · LATCH

Why, and examples

Cummings SNUG-1999 §synthesis mismatch. A signal left unassigned on some path through a combinational block must hold its previous value, which is a latch -- inferred silently, sensitive to a clock it was never meant to have, and usually found in static timing analysis rather than simulation.

good
always_comb begin
    valid_o = 1'b0;         // Default
    data_o  = '0;           // Default
    if (hit_i) begin
        valid_o = 1'b1;
        data_o  = payload_i;
    end
end
bad
always_comb begin
    if (hit_i) begin
        valid_o = 1'b1;       // valid_o latches when hit_i is low
        data_o  = payload_i;
    end
end

Sources: Cummings SNUG-1999 §synthesis mismatch, lowRISC §Combinational Logic

MUST

Every case has a default

Every case, casez and unique case statement has a default arm, even when it is empty or only raises an error.

enforced by verible lint · case-missing-default — also verilator · CASEINCOMPLETE

Why, and examples

Without a default, any unlisted value leaves the case's outputs unassigned, which infers a latch in combinational logic and leaves the state unchanged in sequential logic. unique does not substitute: it adds a runtime check in simulation but does not supply a value, and the check is absent in synthesis.

good
unique case (state_q)
    IDLE: state_d = req_i ? BUSY : IDLE;
    BUSY: state_d = done_i ? IDLE : BUSY;
    default: state_d = IDLE;   // Unreachable; recovers from an upset state
endcase
bad
unique case (state_q)
    IDLE: state_d = req_i ? BUSY : IDLE;
    BUSY: state_d = done_i ? IDLE : BUSY;
endcase

Sources: Cummings SNUG-2005, lowRISC §Case Statements, Verilator CASEINCOMPLETE

MUST

Literals state their width

Sized literals state a width that matches their context; a literal is never wider than its declared size, and '0/'1/'x are used for context-width fills.

enforced by verible lint · truncated-numeric-literal undersized-binary-literal numeric-format-string-style

Why, and examples

An unsized literal is 32 bits wide, so comparing or assigning it silently zero-extends or truncates the other operand. Truncation of a constant is the case the compiler is least likely to warn about, because the value looks intentional.

good
logic [7:0] byte_q;
assign byte_q = 8'hFF;
assign wide_q = '0;        // Context width, whatever it is
bad
logic [7:0] byte_q;
assign byte_q = 9'h1FF;    // Truncated to 8'hFF, silently

Sources: lowRISC §Number Formatting, Verilator WIDTH

MUST

casex is not used; casez is used sparingly

casex does not appear in synthesizable RTL. casez is used only where the truth table genuinely has don't-care bits, with don't-cares written as ? and never as z.

enforced by verilator · CASEX

Why, and examples

Cummings SNUG-1999. casex treats x as a don't-care in the case *expression* as well as the items, so an input that is still unknown silently matches the first arm -- in his cited case an input unknown after reset release was masked in RTL simulation, gate-level simulation missed it, and the first ASIC came back flawed. casez has the same mechanism but only a floating z can trigger it, which is far less likely to mask a reset bug.

good
casez (opcode_i)
    8'b0000_????: op_d = OP_ALU;
    8'b0001_????: op_d = OP_LOAD;
    default:      op_d = OP_ILLEGAL;
endcase
bad
casex (opcode_i)
    8'b0000_xxxx: op_d = OP_ALU;   // Matches when opcode_i is itself unknown
endcase

Sources: Cummings SNUG-1999 §casex

MUSTAUTOFIX

Functions and tasks are automatic with typed arguments

Every function and task declares an explicit automatic lifetime, an explicit return type, and an explicit direction and type on each argument.

enforced by verible lint · explicit-function-lifetime explicit-task-lifetime explicit-function-task-parameter-type

Why, and examples

A static-lifetime function shares one copy of its locals across every call, so two concurrent calls corrupt each other -- and static is the default in a module. Explicit types stop the implicit single-bit default from silently truncating a wider argument.

good
function automatic logic [7:0] parity_byte(input logic [7:0] data);
    return {^data, data[6:0]};
endfunction
bad
function parity_byte(data);
    parity_byte = {^data, data[6:0]};
endfunction

Sources: lowRISC §Functions and Tasks, Verible

MUST

Parameters declare a storage type and the right keyword

parameter appears only in a module's formal parameter list; constants inside a module body are localparam. Both declare an explicit type.

enforced by verible lint · explicit-parameter-storage-type proper-parameter-declaration

Why, and examples

A parameter in a module body is overridable through the deprecated defparam path and confuses which values are part of the module's interface. An untyped parameter takes its type from its default value, so overriding 8 with 8'sd8 silently changes the arithmetic of every expression using it.

good
module fifo #(
    parameter int unsigned Depth = 8       // Interface: tunable
) ();
    localparam int unsigned PtrW = $clog2(Depth);  // Body: derived
bad
module fifo ();
    parameter Depth = 8;

Sources: lowRISC §Parameterized Modules, Verible

MUST

Dimensions are declared big-endian and non-negative

Packed dimensions are declared [N-1:0] and unpacked dimensions [0:N-1] or [N]; no dimension may evaluate to a negative size.

enforced by verible lint · packed-dimensions-range-ordering unpacked-dimensions-range-ordering forbid-negative-array-dim

Why, and examples

Mixing endianness across a design makes bit 0 mean the LSB in one module and the MSB in the next, so a part-select that looks symmetric reverses the field. A negative dimension comes from a parameter expression that underflows -- $clog2(1) is 0, so [$clog2(Depth)-1:0] is illegal at Depth = 1, which is the edge case the parameterised module is least likely to be tested at.

good
logic [31:0] word_q;              // Packed: big-endian
logic [7:0]  buffer_q [0:15];     // Unpacked: little-endian
bad
logic [0:31] word_q;

Sources: lowRISC §Dimensions, Verible

MUST

No simulation-only system tasks in synthesizable code

Synthesizable RTL contains no system tasks or functions that exist only for simulation, and no # delays.

enforced by verible lint · invalid-system-task-function — also verilator · ASSIGNDLY STMTDLY

Why, and examples

Synthesis ignores delays and most system tasks rather than rejecting them, so the gates implement something the RTL simulation never showed. A # delay in RTL is also the classic way a race is hidden rather than fixed.

good
assign data_o = data_q;
bad
assign #1 data_o = data_q;

Sources: Cummings SNUG-2000 guideline #8, Verible

SHOULDAUTOFIX

`default_nettype none opens every file

Each .sv file begins with default_nettype none after the header and ends with resetall.

enforced by svstyle · default_nettype — also verilator · IMPLICIT

Why, and examples

Without it, a misspelled identifier on the left of a continuous assignment creates a one-bit implicit net rather than an error -- the design compiles, and the signal is silently stuck. lowRISC does not use the directive (zero uses across OpenTitan, Ibex and PULP) but does mandate that "A correct design contains no inferred nets"; this is that rule made mechanical.

good
`default_nettype none
module fifo_sync ( ... );
endmodule
`resetall
bad
module fifo_sync ( ... );
    assign data_ou = data_q;   // typo: creates a 1-bit implicit net

Sources: house style, lowRISC §Declare all signals (states the rule, not the directive), Verilator IMPLICIT

MUST

Operand widths match; selects stay in range

Both sides of an assignment and the operands of an operator have the same width, or the code states the conversion with a cast (W'(x)), a replication or an explicit zero-extension; every part- and bit-select lies within the declared range.

enforced by verilator · WIDTH WIDTHTRUNC WIDTHEXPAND WIDTHXZEXPAND WIDTHCONCAT SELRANGE

Why, and examples

An implicit width change is silent in both simulation and synthesis: a wider value is truncated and a narrower one is zero- or sign-extended by rules most readers cannot recite. The design compiles, and the missing top bit shows up as wrong arithmetic at the first large value. An out-of-range select reads x in simulation and an arbitrary bit in gates.

good
logic [3:0] nibble_o;
assign nibble_o = data_i[3:0];          // Width stated by the select
assign sum_d    = 9'(a_i) + 9'(b_i);    // Carry kept by the cast
bad
logic [3:0] nibble_o;
assign nibble_o = data_i;               // 8 bits into 4, silently truncated

Sources: Verilator WIDTH, lowRISC §Number Formatting

MUST

Every signal that is read has a driver

No signal is read without being driven; an input that is intentionally constant is tied off explicitly where it is declared or connected.

enforced by verilator · UNDRIVEN — also yosys · no-driver

Why, and examples

An undriven net simulates as z or x and synthesis replaces it with a constant of its own choosing, usually zero, then optimises away everything that depended on it. The gates and the simulation both look plausible and disagree with each other and with the author.

good
logic busy_q;  // Driven by the state register below
assign busy_o = busy_q;
bad
logic busy_q;  // Declared, never assigned
assign busy_o = busy_q;

Sources: Verilator UNDRIVEN, house style

SHOULD

Every declared signal is used

A declared signal is read somewhere; a signal or input bit that is deliberately unused carries a comment and a scoped Verilator waiver.

enforced by verilator · UNUSEDSIGNAL UNUSEDGENVAR

Why, and examples

An unused signal is either dead code or a connection that was meant to be made. The second is a bug that no other check reports, and it hides among the first unless the dead code is removed or waived where it stands.

good
// verilator lint_off UNUSEDSIGNAL
logic [7:0] spare_q;  // Reserved for the next revision
// verilator lint_on UNUSEDSIGNAL
bad
logic [7:0] parity_q;  // Computed, then never connected to parity_o

Sources: Verilator UNUSEDSIGNAL, house style

MUST

Signed arithmetic is declared, never mixed

A signal that holds a two's-complement value is declared signed, and an expression that mixes it with unsigned operands converts them explicitly with $signed() / $unsigned().

enforced by review only

Why, and examples

IEEE 1800 evaluates an expression as signed only when every operand is signed: one unsigned operand -- a port, a part-select, a sized literal like 4'd1 -- silently makes the whole expression unsigned, so a negative value compares as a large positive one and a right shift fills with zeros. The code reads correctly and simulates wrongly only for negative inputs, which are the ones a directed test forgets.

good
logic signed [7:0] offset_i;
logic signed [8:0] sum;
assign sum = $signed({1'b0, base_i}) + offset_i;   // Both operands signed
bad
logic signed [7:0] offset_i;
assign sum = base_i + offset_i;                    // base_i unsigned: all unsigned

Sources: IEEE 1800-2017 §11.8.1 (expression type rules), house style

SYNSynthesis and portability
MUST

full_case and parallel_case are never used

No case statement carries a full_case or parallel_case synthesis directive.

enforced by svstyle · synthesis_directives

Why, and examples

Cummings SNUG-1999, "the evil twins". They live in comments, so the simulator ignores them while synthesis acts on them -- the design is described differently to the two tools by construction. full_case lets synthesis delete logic for values it now believes unreachable (his decoder loses its enable input entirely); parallel_case deletes a priority encoder that RTL simulation still models. They are most dangerous when they work, because nothing reports the divergence.

good
unique case (sel_i)
    2'b00: y_o = a_i;
    2'b01: y_o = b_i;
    default: y_o = '0;
endcase
bad
case (sel_i)  // synopsys full_case parallel_case
    2'b00: y_o = a_i;
    2'b01: y_o = b_i;
endcase

Sources: Cummings SNUG-1999 §full_case parallel_case

SHOULD

unique and priority replace the directives, with care

Use unique case with a default as the normal form. priority case states intentional overlap; plain case is used where neither claim holds.

enforced by review only — also verilator · CASEOVERLAP

Why, and examples

Cummings SNUG-2005: these are real keywords, so simulation, synthesis and formal all read them, and they double as runtime assertions. But they are not a blanket substitute -- a default nullifies priority entirely (only unique's overlap check survives), neither removes a latch caused by unassigned outputs, and both raise runtime errors against the standard default-assign-then-partial-case idiom.

good
unique case (state_q)
    IDLE: state_d = BUSY;
    BUSY: state_d = IDLE;
    default: state_d = IDLE;
endcase
bad
priority case (state_q)
    IDLE: state_d = BUSY;
    default: state_d = IDLE;   // `default` makes `priority` mean nothing here
endcase

Sources: Cummings SNUG-2005 §unique & priority

SHOULD

Priority logic is written as if/else, parallel logic as case

Intentional priority is written as an if/else if chain; a case statement is reserved for logic whose arms are genuinely mutually exclusive.

enforced by review only

Why, and examples

The construct states the designer's intent to every downstream tool and reader. A priority chain written as a case invites someone to add parallel_case later to "optimise" it, which is exactly the bug SYN-001 describes.

good
if (err_i) begin
    resp_d = RESP_ERR;
end else if (valid_i) begin
    resp_d = RESP_OK;
end
bad
case (1'b1)
    err_i:   resp_d = RESP_ERR;   // Overlapping arms in a `case`
    valid_i: resp_d = RESP_OK;
endcase

Sources: Cummings SNUG-1999

MUST

Modules are not instantiated recursively

A module does not instantiate itself, directly or through a cycle.

enforced by review only

Why, and examples

Elaboration of a recursive hierarchy terminates only if the tool can prove the parameter sequence reaches the base case, and the tools disagree about which expressions they can prove. What elaborates in one synthesiser hangs in the next.

good
// Write the recursion as a generate-for over a flat array of stages.
bad
module tree #(parameter int N = 8) ();
    tree #(.N(N/2)) u_left ();

Sources: lowRISC §Generate Constructs

MUST

No combinational loops

No signal depends combinationally on itself: every feedback path passes through a flop.

enforced by yosys · logic-loop

Why, and examples

A combinational loop has no stable value to settle to, or several; it oscillates or latches depending on gate delays that neither simulation nor static timing analysis models. RTL simulation usually shows a plausible value or a delta-cycle limit, and the synthesis tool breaks the loop wherever it likes. The usual source is two always_comb blocks, or a ready/valid pair, that each read what the other drives.

good
assign ready_o = ~full_q;          // Depends only on a flop
bad
assign ready_o = valid_i & ~stall; // stall is computed from ready_o
assign stall   = ready_o & busy_i;

Sources: Yosys check, house style

SHOULD

No initial blocks in synthesizable RTL

Synthesizable RTL gives state its starting value through reset, not an initial block. Simulation-only code that needs one sits under ifndef SYNTHESIS, and elaboration-time checks go through ASSERT_INIT or a generate-time $error.

enforced by svstyle · initial_block

Why, and examples

ASIC synthesis ignores initial blocks, so a design that relies on one simulates from a value the silicon never has, and the first sign is a chip that does not come out of power-on the way the testbench did. FPGA flows do honour initial values for registers, which is why this is a SHOULD: a deliberate FPGA-only use carries a waiver saying so.

good
always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
        count_q <= '0;       // Starting value from reset
bad
initial count_q = '0;    // Ignored by ASIC synthesis

Sources: IEEE 1800-2017 §9.2.1 (initial procedures), house style

RSTReset and clocking
MUST

Resets are asynchronous and active-low

Flip-flops are reset asynchronously by an active-low reset named rst_ni, written as always_ff @(posedge clk_i or negedge rst_ni) with the reset test as the first if in the block.

enforced by svstyle · reset_style

Why, and examples

This is the ecosystem's convention and measurement confirms it is followed, not just written: 902 of 959 reset always_ff blocks in OpenTitan and 108 of 130 in Ibex use async active-low, with exactly one positive-edge async reset in all of OpenTitan's hw/ip. Active-low matters physically -- a reset asserted by a pin being pulled low survives loss of the driver. Note that BSG argues the opposite (synchronous, high-true) precisely to avoid the de-assertion hazard RST-002 addresses; this style takes the async path and mandates the synchroniser instead.

good
always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
        count_q <= '0;
    end else begin
        count_q <= count_d;
    end
end
bad
always_ff @(posedge clk_i) begin
    if (rst) begin      // synchronous, active-high
        count_q <= '0;

Sources: lowRISC §Resets, Cummings SNUG-2003 §reset techniques, OpenTitan/Ibex measurement (94.1% / 83.1%)

MUST

Every asynchronous reset passes through a reset synchroniser

An asynchronous reset entering a clock domain is asserted asynchronously but de-asserted synchronously, through a two-flop reset synchroniser in that domain.

enforced by review only

Why, and examples

Cummings SNUG-2003, verbatim: "EVERY ASIC USING AN ASYNCHRONOUS RESET SHOULD INCLUDE A RESET SYNCHRONIZER CIRCUIT!!" Assertion is safe because it bypasses the clock path. Removal is the bug, two ways: de-asserting inside a flop's reset *recovery* window leaves it metastable, so the chip silently loses its reset state -- an intermittent field failure the controlled simulation never reproduces; and an asynchronous removal lets clock- and reset-tree skew release some flops a cycle before others, starting the design in a state nobody simulated. The synchroniser collapses this into one closed timing budget.

good
// Reset synchroniser: async assert, sync de-assert into clk_i.
always_ff @(posedge clk_i or negedge rst_ext_ni) begin
    if (!rst_ext_ni) begin
        {rst_q, rst_meta_q} <= 2'b00;
    end else begin
        {rst_q, rst_meta_q} <= {rst_meta_q, 1'b1};
    end
end
bad
// External async reset driven straight into every flop in the domain.
always_ff @(posedge clk_i or negedge rst_ext_ni) begin

Sources: Cummings SNUG-2003 §reset synchronizer

MUST

Each clock domain has its own reset synchroniser

In a multi-clock design, every clock domain instantiates its own reset synchroniser and its own reset distribution tree.

enforced by review only

Why, and examples

A reset de-asserted synchronously to one clock is asynchronous to every other, so sharing one synchroniser reintroduces exactly the recovery-window failure it was there to prevent -- in the domains nobody looked at.

good
rst_sync u_rst_sync_sys  (.clk_i(sys_clk_i),  .rst_ni(rst_ext_ni), .rst_no(sys_rst_ni));
rst_sync u_rst_sync_usb  (.clk_i(usb_clk_i),  .rst_ni(rst_ext_ni), .rst_no(usb_rst_ni));
bad
// One synchroniser in the sys domain; usb flops use sys_rst_ni.

Sources: Cummings SNUG-2003

SHOULD

Only control state is reset

Reset the flops that must hold a known value at startup -- state machines, counters, valid bits, configuration -- and leave datapath registers unreset.

enforced by review only

Why, and examples

Every reset flop costs a reset-tree endpoint, routing and a larger cell, and the reset tree is one of the hardest nets to close timing on. A datapath register whose value is qualified by a valid bit does not need a defined value at startup, because nothing reads it until the valid bit says so.

good
always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
        valid_q <= 1'b0;      // Control: reset
    end else begin
        valid_q <= valid_d;
    end
end
always_ff @(posedge clk_i) begin
    data_q <= data_d;       // Datapath: qualified by valid_q, not reset
end
bad
// Every flop in a 512-bit datapath tied to the reset tree.

Sources: Xilinx UG949 §reset strategy, OpenTitan practice (57 unreset always_ff blocks)

MUST

Reset and clock nets are not mixed

A signal used as an asynchronous reset is not also used as ordinary data, and a clock is not gated with combinational logic; clock gating uses a dedicated integrated clock-gating cell.

enforced by verilator · SYNCASYNCNET

Why, and examples

A net that reaches both a reset pin and a data pin cannot be timed as either, and the synthesis tool picks one. A clock gated by if (en) in RTL produces glitches on the gated clock that the RTL simulation cannot show, because RTL has no notion of a glitch.

good
prim_clock_gating u_cg (
    .clk_i    (clk_i),
    .en_i     (enable_q),
    .clk_o    (clk_gated)
);
bad
assign clk_gated = clk_i & enable_q;   // Glitches on every enable change

Sources: Cummings SNUG-2001, Verilator SYNCASYNCNET

FSMState machines
MUST

State is a typed enum with _q/_d

FSM state is held in a variable of a typedef enum type, named state_q with next state state_d. State encodings are never `define macros.

enforced by review only

Why, and examples

Cummings SNUG-2003 §SystemVerilog FSM. A typed enum gives the tool a closed set of legal values, so an assignment of an out-of-range constant is an error and waveform viewers show state names instead of numbers. Macros have no type, no scope and no tool visibility.

good
typedef enum logic [1:0] { IDLE, BUSY, DONE } state_e;  // Transfer states
state_e state_q, state_d;                               // Current/next state
bad
`define IDLE 2'b00
logic [1:0] state;

Sources: Cummings SNUG-2003 §SystemVerilog FSM, lowRISC §Finite State Machines

SHOULD

FSMs use the two- or three-block style

An FSM is written as a sequential always_ff holding the state register plus a combinational always_comb computing next state and outputs; a third always_ff is added when outputs must be registered.

enforced by review only

Why, and examples

Cummings SNUG-2000 §FSM. Separating the register from the next-state logic means the reset, the clocking and the state transition each appear exactly once, and the combinational block can be read as a pure function of current state and inputs. A single-block FSM makes every output a registered output whether or not that was intended.

good
// State register.
always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
        state_q <= IDLE;
    end else begin
        state_q <= state_d;
    end
end

// Next-state and output logic.
always_comb begin
    state_d = state_q;
    ...
end
bad
always_ff @(posedge clk_i) begin
    case (state_q)
        IDLE: if (req_i) begin state_q <= BUSY; out_q <= 1'b1; end

Sources: Cummings SNUG-2000 §FSM coding styles

SHOULD

FSM outputs crossing a module boundary are registered

An FSM does not drive combinational (Mealy) outputs across a module boundary; such outputs are registered first, or the output logic moves into the consuming module.

enforced by review only

Why, and examples

Cummings SNUG-2000. A combinational output depends on the FSM's inputs in the same cycle, so it arrives late and its timing path continues into whatever the consumer does with it -- a path that neither module's timing constraints describe on its own.

good
always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
        grant_o <= 1'b0;
    end else begin
        grant_o <= (state_d == GRANTED);
    end
end
bad
assign grant_o = (state_q == IDLE) && req_i;   // Mealy, leaves the module

Sources: Cummings SNUG-2000 §FSM outputs

SHOULD

One FSM per module

A module contains at most one state machine.

enforced by review only

Why, and examples

Two FSMs in one module share a namespace and a reset block, so their states, transitions and outputs interleave in the source and neither can be read on its own. Splitting them also makes each one's interface -- and therefore its contract -- explicit.

good
// arbiter_fsm.sv and refill_fsm.sv, instantiated side by side.
bad
// One module holding state_q and mode_q, each a separate FSM.

Sources: Cummings SNUG-2000, lowRISC §Finite State Machines

CDCClock domain crossing
MUST

Single-bit crossings go through a two-flop synchroniser

Every one-bit signal crossing an asynchronous clock boundary passes through two flip-flops clocked by the destination clock, with no logic between them.

enforced by review only

Why, and examples

Cummings SNUG-2001. Across asynchronous domains a setup/hold violation on the first flop is a certainty, not a risk -- the phase relationship changes continuously. The second flop gives the first a full clock period for metastability to decay before anything samples it. Without it, a metastable output fans out and different gates resolve it differently, so the design disagrees with itself.

good
always_ff @(posedge dst_clk_i or negedge dst_rst_ni) begin
    if (!dst_rst_ni) begin
        {sync_q, meta_q} <= 2'b00;
    end else begin
        {sync_q, meta_q} <= {meta_q, src_signal_i};
    end
end
bad
always_ff @(posedge dst_clk_i) begin
    captured_q <= src_signal_i;   // One flop: metastability reaches the logic
end

Sources: Cummings SNUG-2001 §synchronizers

MUST

A multi-bit bus is never crossed bit by bit

A multi-bit value is not crossed by putting a two-flop synchroniser on each bit.

enforced by review only

Why, and examples

Cummings SNUG-2001, and the most expensive mistake in this family because every bit is individually correct. Bit-to-bit skew from routing, rise/fall differences and across-die process variation cannot be matched away; a destination edge landing inside that skew window captures some bits this cycle and the rest next cycle, latching a word that never existed in the source. The same breaks paired controls -- a load and an enable arriving in different cycles means nothing loads.

good
// Gray-coded pointer: only one bit changes per increment, so a skewed
// capture yields either the old or the new value, never a third.
prim_fifo_async u_cdc_fifo ( ... );
bad
for (genvar i = 0; i < 8; i++) begin : gen_sync
    prim_flop_2sync u_sync (.d_i(data_i[i]), .q_o(data_sync[i]));
end

Sources: Cummings SNUG-2001 §multi-bit signals

MUST

Multi-bit crossings use a documented pattern

A multi-bit crossing uses one of: consolidation to a single bit regenerated locally; an unsynchronised data bus held stable behind a synchronised enable; Gray coding for counters; or an asynchronous FIFO.

enforced by review only

Why, and examples

Cummings SNUG-2001/2002, in his order of preference. Each pattern removes the skew problem a different way: consolidation leaves nothing multi-bit to skew; the held-stable formulation guarantees the bus is not changing while it is sampled; Gray coding guarantees at most one bit differs between consecutive values; the FIFO does the pointer crossing in Gray code and keeps the data in memory that is never crossed at all.

good
// MCP: data held stable while a synchronised enable qualifies it.
assign data_stable_d = load_i ? data_i : data_stable_q;
prim_flop_2sync u_en_sync (.d_i(load_toggle_q), .q_o(load_toggle_sync));
bad
// A free-running binary counter synchronised straight across.

Sources: Cummings SNUG-2001, Cummings SNUG-2002 §async FIFO

MUST

Signals are registered before they cross

A signal is registered in its source clock domain immediately before crossing; combinational logic does not drive a crossing directly.

enforced by review only

Why, and examples

Cummings SNUG-2001. Combinational output settles through a sequence of intermediate values, and every one of them is a potential edge for the destination to capture -- the crossing sees phantom pulses that never existed as logical values. Registering also improves MTBF by cutting how often the crossed signal changes.

good
always_ff @(posedge src_clk_i) begin
    req_q <= req_valid && !busy;   // Registered, then crossed
end
bad
assign req_cross = req_valid && !busy;   // Glitches cross as real edges

Sources: Cummings SNUG-2001

MUST

Open-loop pulses are wide enough to be captured

A one-bit signal crossed open-loop through a two-flop synchroniser is held for more than 1.5 destination clock periods -- three destination edges. Narrower events use a closed-loop handshake.

enforced by review only

Why, and examples

Cummings SNUG-2001. A pulse shorter than the destination period can fall entirely between two destination edges and be missed, and one sampled near an edge may be captured by the first flop but not held long enough for the second. Three edges is the width that guarantees capture regardless of phase.

good
// Toggle: the level survives any number of destination periods.
assign req_toggle_d = req_i ? ~req_toggle_q : req_toggle_q;
bad
// A single source-clock-wide pulse crossing into a slower domain.

Sources: Cummings SNUG-2001

SHOULD

Crossings are confined to dedicated modules

Only the top level of a design sees more than one clock; every clock-domain crossing lives inside a module whose purpose is that crossing.

enforced by review only

Why, and examples

Cummings SNUG-2001. Confining crossings makes them findable -- the set of modules to review for CDC is a list rather than a search -- and it lets the rest of the design be verified, timed and lint-checked as single-clock logic.

good
// cdc_sync.sv, cdc_fifo.sv instantiated at the top; leaves are single-clock.
bad
// A leaf module taking both sys_clk_i and usb_clk_i.

Sources: Cummings SNUG-2001 §partitioning

GENGenerate and parameterisation
MUST

Every generate block is labelled

Every generate for body and every branch of a generate if carries a lower_snake_case label prefixed gen_, written as begin : gen_name.

enforced by verible lint · generate-label generate-label-prefix

Why, and examples

The label becomes part of the hierarchical path of everything inside it. Without one the tool invents a name like genblk3, so waveforms, timing reports, constraints and bug reports all address the logic by a number that shifts when an unrelated generate is added above it.

good
for (genvar i = 0; i < NumPorts; i++) begin : gen_port
    prim_fifo_sync u_fifo ( ... );
end
bad
for (genvar i = 0; i < NumPorts; i++) begin
    prim_fifo_sync u_fifo ( ... );
end

Sources: lowRISC §Generate Constructs, Verible

MUSTAUTOFIX

Generate constructs use the modern form

generate/endgenerate regions are not written, genvar is declared inline in the for header, and a generate body is not wrapped in an extra bare begin block.

enforced by verible lint · legacy-generate-region legacy-genvar-declaration v2001-generate-begin

Why, and examples

The generate region has been optional since Verilog-2001 and adds a scope level that changes hierarchical paths without changing behaviour. An inline genvar scopes the loop variable to its loop, so two loops in one module cannot collide.

good
for (genvar i = 0; i < N; i++) begin : gen_stage
    ...
end
bad
genvar i;
generate
    for (i = 0; i < N; i++) begin : gen_stage
        ...
    end
endgenerate

Sources: lowRISC §Generate Constructs, Verible

MUST

Parameter legality is asserted, not assumed

A parameter with restricted legal values carries an assertion or an elaboration-time check stating the restriction.

enforced by review only

Why, and examples

A parameterised module is instantiated by people who did not read it. A power-of-two depth assumption that is violated produces wrong hardware rather than an error, and the failure appears as a functional bug in the instantiating design, far from the module that assumed it.

good
`ASSERT_INIT(DepthIsPowerOfTwo_A, (Depth & (Depth - 1)) == 0)
bad
parameter int unsigned Depth = 8;  // must be a power of two (comment only)

Sources: lowRISC §Parameterized Modules

VERAssertions and verification
SHOULD

Assertions are written through the standard macros

SVA in RTL is written with the project's assertion macros (ASSERT, ASSERT_INIT, ASSERT_KNOWN) rather than bare assert property`.

enforced by review only

Why, and examples

The macros carry the clock and reset arguments, the severity, and the `ifdef guards that keep assertions out of synthesis. Bare assertions have to repeat all of that at every site, and the one that forgets the reset guard fires spuriously during reset in every simulation.

good
`ASSERT(ReqStableUntilAck_A, req_o && !ack_i |=> req_o, clk_i, !rst_ni)
bad
assert property (@(posedge clk_i) req_o && !ack_i |=> req_o);

Sources: lowRISC §Assertions, PULP assertions.svh

SHOULD

Module outputs are checked for X

Module outputs carry an `ASSERT_KNOWN check, except outputs that may legitimately be unknown before first write, such as the read data of a FIFO or a memory.

enforced by review only

Why, and examples

X propagates silently through most RTL operators and is optimistically resolved by others, so an X originating in one module is often first noticed as wrong data several modules downstream. Checking at the boundary localises it to the module that produced it.

good
`ASSERT_KNOWN(ValidKnown_A, valid_o)
bad
// No X checks; an uninitialised counter shows up as wrong data three
// modules away.

Sources: lowRISC §Assertions

MUST

Lint runs clean before code is shared

svstyle check reports no MUST violations and verilator --lint-only -Wall reports no warnings before RTL is committed or handed over.

enforced by review only

Why, and examples

Every check in this guide is cheap at the point of writing and expensive later: a latch found by static timing analysis, a width truncation found in a chip bring-up lab, a missing case default found by a customer. The gate exists so that none of them travels.

good
$ svstyle check fifo_sync.sv && verilator --lint-only -Wall fifo_sync.sv
bad
// Committed with 14 SHOULD findings and 2 MUSTs outstanding.

Sources: house style

§05

The tools behind the rules

svstyle runs each tool, translates what it says into house rule IDs, and prints one report. What no tool can see is left to the skill's judgement list and to review.

Who enforces the 82 rules

18
24
9
29
svstyle · 18verible format · 1verible lint · 24verilator · 9yosys · 1review only · 29

svstyle

this repo

The checker and fixer shipped with the skill. It owns the rules no open-source tool covers — the file header, port and declaration comments, port order, named connections, reset style — and folds every other tool's output into one report keyed by house rule IDs.

runs
svstyle check · svstyle fix · svstyle rules
get it
uv tool install svstyle or pipx install svstyle
owns 18
DOC-001DOC-002DOC-003DOC-004DOC-005DOC-006STR-002STR-005PRT-001PRT-002PRT-003NAM-003NAM-010LNG-001LNG-014SYN-001SYN-006RST-001
backs up
PRT-005FMT-001LNG-002LNG-003

verible format

external · pinned

CHIPS Alliance's formatter owns layout: indentation, wrapping, and the tabular alignment of port lists, parameters and named connections. check reports a file it would change as FMT-002; fix runs it, then verifies that every token change is one it allows, or writes nothing.

runs
verible-verilog-format with the flags in src/svstyle/data/verible-format.flags
get it
v0.0-3946-g851d3ff4 from the Verible releases page; another version warns that alignment may drift. fix refuses to run without it.
owns 1
FMT-002

verible lint

external · pinned

The style linter: naming, always_comb/always_ff usage, begin/end, file names, literal widths. Each Verible rule name maps to the house ID that claims it.

runs
verible-verilog-lint with src/svstyle/data/rules.verible_lint
get it
v0.0-3946-g851d3ff4, same release as the formatter.
owns 24
DOC-010STR-001PRT-005NAM-001NAM-002NAM-004NAM-005NAM-006NAM-007NAM-008FMT-001FMT-003FMT-004FMT-005LNG-002LNG-003LNG-007LNG-008LNG-010LNG-011LNG-012LNG-013GEN-001GEN-002

verilator

external

--lint-only -Wall over every file of the design at once: widths, latches, multiple drivers, undriven and unused signals, missing pins. It is also the simulator for the skill's self-checking testbench step.

runs
verilator --lint-only -Wall (all files together so submodules elaborate)
get it
Verilator 5; CI uses 5.053 from the OSS CAD Suite.
owns 9
PRT-004LNG-004LNG-005LNG-006LNG-009LNG-015LNG-016LNG-017RST-005
backs up
STR-001PRT-005LNG-003LNG-007LNG-013LNG-014SYN-002

yosys

external

Synthesis is the contract. --synth, or synth = "yosys" in .svstyle.toml, runs read_verilog -sv; hierarchy; proc; check: combinational loops, conflicting drivers, and whether Yosys reads the code at all (EXT-YOSYS-read, a MUST). With synth = "yosys-slang" the read is plugin -i slang; read_slang, which accepts nearly all synthesizable SystemVerilog. The subset the skill writes is backed by 149 one-construct probes proved with sat, rerun by CI on both frontends.

runs
yosys, or yosys + the slang plugin
get it
Yosys 0.69 from the OSS CAD Suite (which also ships yosys-slang); Homebrew's Yosys has no slang plugin.
owns 1
SYN-005
backs up
LNG-005LNG-016

review only

Claude + you

What no tool can decide: reset scope, FSM structure, CDC patterns, signed arithmetic, whether a name says what it means. The skill walks this list before it calls the work done, and a human reviewer reads the same IDs.

runs
svstyle rules --unmapped lists them
get it
nothing to install
owns 29
DOC-007DOC-008DOC-009STR-003STR-004PRT-006PRT-007NAM-009LNG-018SYN-002SYN-003SYN-004RST-002RST-003RST-004FSM-001FSM-002FSM-003FSM-004CDC-001CDC-002CDC-003CDC-004CDC-005CDC-006GEN-003VER-001VER-002VER-003

Tool findings without a house rule

A Verible or Verilator warning no rule claims passes through as EXT-VL-<rule> or EXT-VLTR-<CODE> at SHOULD. EXT-VLTR-ERROR (MUST) means the design did not elaborate: fix it first, the rest of the report is incomplete. EXT-VLTR-MODMISSING means a submodule's file was not on the command line. Missing tools are skipped with a warning by check; --no-external runs only svstyle's own checks.

§06

svstyle reference

Everything the skill runs is a normal CLI you can run yourself, in a terminal or in CI.

commands
svstyle check rtl/                 # report; exit 1 on any MUST finding
svstyle check --synth rtl/*.sv     # also run Yosys (loops, drivers, readability)
svstyle check -f files.f -I inc    # a Verilator/VCS filelist and include dirs
svstyle fix rtl/ --diff            # show what fix would change; exit 1 if anything
svstyle fix rtl/                   # format, insert header/comments, modernise legacy code
svstyle rules --id PRT-003         # one rule in full
svstyle rules --unmapped           # the rules that need human judgement

A directory stands for every *.sv below it. --select/--ignore take IDs or prefixes, ruff-style: --select DOC,PRT-003. --json gives machine-readable output; --no-external runs only svstyle's own checks.

output
rtl/fifo.sv:14:1: DOC-003 `rst_ni` has no trailing // comment [fix available]
rtl/fifo.sv:31:5: LNG-015 Operator ASSIGN expects 4 bits, got 5 (WIDTHTRUNC)
2 findings (MUST 2, SHOULD 0)
.svstyle.toml — the nearest one walking up from each file
company = "ACME Corp"      # the copyright holder in every file header
synth   = "yosys"          # or "yosys-slang"; either runs Yosys on every check
select  = ["DOC", "LNG"]   # optional: only these rule IDs/prefixes
ignore  = ["DOC-005"]      # optional: drop these
include = ["rtl/inc"]      # optional: `include dirs, relative to this file
exclude = ["tb/*"]         # optional: skip when walking a directory or filelist

An unknown key or a wrong type is an error (exit 2), so a typo never silently disables a setting.

exitkindmeaning
0cleanno MUST findings (SHOULD allowed)
1violationsat least one MUST finding: fix and rerun
2errorunreadable file, bad config, or a required tool missing
3refusedthe file does not parse; a missing `include needs -I DIR
4bugsvstyle broke its own invariant; nothing was written

Waivers

rtl/mac.sv
// svstyle: waive RST-001 DSP blocks need a synchronous reset
always_ff @(posedge clk_i) begin
    acc_q <= acc_d;
end

logic pad_io;  // svstyle: waive LNG-001 bidirectional pad

The tools' own pragmas (// verilog_lint: waive, // verilator lint_off) also work.

In CI

pre-commit and baselines
# .pre-commit-config.yaml
- repo: https://github.com/leandrotozzi/sv-skill
  rev: v0.1.0
  hooks:
    - id: svstyle-check   # and/or svstyle-fix

# adopt the style on existing code: only new findings fail
svstyle check rtl/ --baseline .svstyle-baseline --write-baseline
svstyle check rtl/ --baseline .svstyle-baseline

svstyle fix --diff exits 1 when any file would change, so it gates formatting too.