astli 0.3.0

SystemVerilog language tooling: a lossless syntax tree, a preprocessor, and a formatter
Documentation

astli

SystemVerilog language tooling in pure Rust: a preprocessor, a lossless syntax tree, and a formatter built on them.

The documentation covers the astli command; the libraries are documented on docs.rs.

Why

A Rust tool that wants to understand SystemVerilog has had to bind a C++ frontend. That works, but the FFI hands back opaque objects: walking a syntax tree means writing a binding for every node and method you touch, and the C++ build is slow and hard to cache. bender is where that friction showed up.

astli is the frontend as a Rust library. The tree is ordinary Rust data that keeps every byte of the source, comments and whitespace included, so a tool can walk it, query it, and print it back exactly.

The formatter is the first thing built on it, and a linter the second. A language server, or deeper analysis, could follow.

Scope

astli lexes, preprocesses and parses; it does not elaborate or type-check. slang is the state of the art for SystemVerilog, a complete compiler, and the right tool whenever you need one. astli does not try to replace it.

Status

Pre-1.0, and the API still changes. What exists:

  • Preprocessor. All directives, macro expansion, includes, and filelists with +incdir+ and +define+.
  • Parser. Design units, declarations, classes, instances, generate blocks, statements and expressions, constraints, assertions, covergroups and clocking blocks. Rarer constructs such as specify, checker, gate primitives and UDPs are kept verbatim for now, so nothing is ever lost; docs/grammar-coverage.md has the detail.
  • Formatter. The lowRISC style. It checks that every result preprocesses to the same thing as its input, and refuses a file rather than change what it means.
  • Linter. Rules for likely bugs and for the lowRISC style, with verible's names where verible has the rule, waived by attribute or by path.
  • Filelists. Trimmed to what a top needs, and put in dependency order.

All of it is tested against open-source designs, fetched by scripts/fetch-corpus.sh.

Install

The astli command comes prebuilt for Linux, macOS and Windows. With the installer script:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/fischeti/astli/releases/latest/download/astli-cli-installer.sh | sh
powershell -ExecutionPolicy Bypass -c "irm https://github.com/fischeti/astli/releases/latest/download/astli-cli-installer.ps1 | iex"

With uv, or to run it once without installing:

uv tool install astli
uvx astli fmt top.sv

With pip, or pipx to install it in an environment of its own or run it once:

pip install astli
pipx install astli
pipx run astli fmt top.sv

With cargo, as a prebuilt binary through cargo-binstall, or from source:

cargo binstall astli-cli
cargo install astli-cli

Formatting

astli fmt top.sv            # print the formatted file
astli fmt -w rtl/*.sv       # rewrite in place
astli fmt --check -f src.f  # fail if any file in a filelist is unformatted
astli fmt --diff top.sv     # show what would change
astli fmt -                 # stdin to stdout

Each file is formatted on its own: includes are not followed and no +define+ reaches the formatter, so the output depends only on the file.

astli-pre-commit runs it as a pre-commit hook.

An astli.toml sets the width, the indentation and how far a column may pad to line up; these are the defaults:

[fmt]
width = 100
indent = 2
max-pad = 12

To keep an item, statement or member as you wrote it, put the attribute (* astli_fmt_skip *) on it. It is left as written and only moved to its indentation:

(* astli_fmt_skip *)
assign out = sel ? a
               : b;

Linting

astli lint -f src.f                 # report what the rules find
astli lint -W correctness rtl/*.sv  # report a group as warnings
astli lint --list                   # every rule, its group and its level

Rules come in groups: correctness fails the run, suspicious and lowrisc warn, restriction is off. To turn a rule off for one construct, put an attribute on it:

(* astli_allow = "always-ff-non-blocking" *)
always_ff @(posedge clk_i) q = d;

An astli.toml sets levels for a project, and per path for what no attribute reaches:

[lint.paths]
"hw/vendor/**" = { allow = ["lowrisc"] }

Filelists

astli files -f design.f --top soc_top          # only what soc_top needs
astli files -f design.f --top soc_top --order  # packages before their users
astli files -f design.f --emit tops            # modules nothing instantiates
astli files -f design.f --top soc_top --why rtl/fifo.sv

Each file is expanded as it would be compiled, with the filelist's +incdir+s and +define+s, so a module a macro instantiates counts. A name used and declared in no file is a warning.

astli pickle takes the same selection and writes the files as one source, each as written with its headers inlined. With --expand, each is written as a compiler reads it instead, and --prefix and --suffix can rename every module, interface, program, package and class the files declare, wherever it is named, so that two designs can share a compilation:

astli pickle -f design.f --top soc_top --order > soc.sv
astli pickle -f design.f --top soc_top --order --expand --prefix v2_ > soc.sv

--exclude-rename NAME keeps a name as it is.

As a library

[dependencies]
astli = "0.2"
use astli::parse::SyntaxTree;
use astli::syntax::ast::{AstNode, ModuleDecl};

let tree = SyntaxTree::read("top.sv")?;

for module in tree.root().descendants().filter_map(ModuleDecl::cast) {
    if let Some(name) = module.name() {
        println!("module {}", name.text());
    }
}

print!("{}", astli::fmt::format(&tree, &Default::default())?);

astli re-exports each astli-* crate as a module; they can also be used on their own. docs/api.md explains the shape of the API.

Development

astli-cli is the workspace's default member, so cargo run needs no -p. Besides fmt, the driver has one subcommand per stage, each printing what that stage made of a file:

cargo run -- lex        top.sv
cargo run -- preprocess top.sv -I include --emit text
cargo run -- parse      top.sv --quiet

The same default member means every other cargo command needs --workspace: cargo nextest run --workspace, not cargo nextest run. -P quick skips the tests that read the corpus. Hooks run through prek: prek install.

docs/plan.md has the architecture and the decisions behind it.

Path
crates/astli-text Spans, the file store, reading a file
crates/astli-diag Rendering a diagnostic, with its macro and include chain
crates/astli-syntax SyntaxKind, the lexer, the rowan tree types
crates/astli-preproc Directives, macros, includes
crates/astli-parse The grammar, and the tree it builds
crates/astli-fmt The formatter
crates/astli-index The top-level names files declare and use
crates/astli-lint Lint rules over one file's tree
crates/astli The umbrella: every library crate, as a module
crates/astli-cli The driver, a binary named astli
docs/ Design and planning
site/ The documentation site, built by scripts/site.sh

Acknowledgements

  • slang, the reference for how SystemVerilog behaves. The preprocessor is tested against it, file by file.
  • rust-analyzer, whose architecture this follows: an event-based parser, a lossless tree, and typed views generated from a tree grammar.
  • rowan, logos and ungrammar, which it is built on.
  • lowRISC's style guide, which the formatter implements.
  • The open-source designs in the test corpus, for being real code.

Licence

MIT or Apache-2.0, at your option. Unless you state otherwise, any contribution you submit is licensed the same way, without further terms.