Crates.io | verusfmt |
lib.rs | verusfmt |
version | 0.5.0 |
source | src |
created_at | 2024-01-29 21:19:05.706294 |
updated_at | 2024-10-24 22:22:11.479285 |
description | An opinionated formatter for Verus |
homepage | https://github.com/verus-lang/verusfmt |
repository | https://github.com/verus-lang/verusfmt |
max_upload_size | |
id | 1119359 |
size | 199,638 |
An opinionated formatter for Verus code.
If you've installed a pre-built binary, you can update to the latest release using
verusfmt --update
We support multiple install methods. Get the latest release using:
Pre-built binaries (recommended)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/verus-lang/verusfmt/releases/latest/download/verusfmt-installer.sh | sh
powershell.exe -ExecutionPolicy RemoteSigned -c "irm https://github.com/verus-lang/verusfmt/releases/latest/download/verusfmt-installer.ps1 | iex"
cargo install verusfmt --locked
These will install the verusfmt
binary. You can then run it on a file using:
verusfmt foo.rs
See verusfmt --help
for more options and details.
verusfmt
may be run in pre-submit scripts, CI, etc., so it can't
be slow.verusfmt
can be
updated and adapted with a reasonable amount of effort.Why not adapt rustfmt
for Verus idioms?
While Verus has Rust-like syntax, it necessarily also deviates from it to
support its idioms naturally, and thus not only would the parser for
rustfmt
need updates, but also careful changes to the emitter would be
needed to have code look natural. Additionally, since practically all Verus
code is inside the verus!{}
macro (and rustfmt
does not easily support
formatting even regular Rust inside macros), a non-trivial amount of effort
would be required to perform the plumbing and maintenance required to
support both formatting outside the verus!{}
macro (as Rust code), while
also formatting Verus code inside the macro.
Does verusfmt
match rustfmt
on code outside the verus!{}
macro?
Yes, by default, verusfmt
handles code inside the verus!{}
macro, and
also runs rustfmt
to handle code outside the macro. Neither should clash
with the other or override each other's formatting changes. Thus, this
makes it easier to incrementally verify small amounts of code inside a
larger unverified Rust crate. You can disable the invocation of rustfmt
using --verus-only
.
Why not build this as a feature of Verus?
By the time Verus receives an AST from rustc
, we've already lost
information about whitespace and comments, meaning that we couldn't preserve
the comments in the reformatted code. Plus, firing up all of rustc
just to
format some code seems heavyweight.
println!
, state_machine!
use
declarations the way rustfmt
doesverusfmt
happens to parse such code, but that is unintentional and cannot be counted upon.rustfmt
Our design is heavily influenced by the Goals above. Rather than write everything from scratch (a notoriously hard undertaking), we use a parser-generator to read in Verus source code, and a pretty-printing library to format it on the way out. We try to keep each phase as performant as possible, and we largely try to keep the formatter stateless, for performance reasons but more importantly to try to keep the code reasonably simple and easy to reason about. Hence we sometimes deviate from Rust's style guidelines for the sake of simplicity.
We define the syntax of Verus source code using this
grammar, which is processed by the Pest
parser generator, which relies on Parsing Expression Grammars
(PEGs). It
conveniently allows us to define our notion of whitespace and comments, which
the remaining rules can then ignore; Pest will handle them implicitly. We
explicitly ignore the code outside the verus!
macro, leaving it to
rustfmt
. We prefer using explicit rules for string constants, since it
allows a more uniform style when formatting the code. In some places, we have
multiple definitions for the same Verus construct, so that we can format it
differently depending on the context (see, e.g., attr_core
). Many of the
rules are designed to follow the corresponding description in The Rust
Reference.
Rather than try to format things ourselves, we rely on the
pretty crate, based on Philip
Wadler's
design for a pretty printer. The core idea is that you create a set of possible
code layouts, and the pretty printer then uses its internal heuristics to pick
the prettiest version. Typically this means that we specify where, say, line breaks
can occur if the code needs to be placed on multiple lines, but you can also
use the group
operator to say that for a particular code snippet, the pretty printer
should also consider placing everying in the group on a single line.
As much as possible, we try to keep the formatter simple by arranging for the formatting of a node to be computed by simply formatting each of its children. Sometimes this requires splitting a node in the parser, so that we can format the same item in two different ways, depending on its context. Rust contexts can be tricky to track dynamically (since Rust allows expressions in statements, and statements in expression), so we try to keep the formatter stateless to reduce the scope for errors.
We welcome contributions! Please read on for details.
We consider it a bug in verusfmt
if you provide verusfmt
with code
that Verus accepts, and verusfmt
either does not accept/parse it, or
produces code that Verus does not accept (or has different semantics from the original).
When this happens, please open a GitHub issue with a minimal example of the offending code
before (and after) formatting.
If verusfmt
produces valid code but you dislike the formatting, please open
a GitHub pull request with your proposed changes and rationale for those changes.
Please ensure that existing test cases still pass (see below for more details),
unless your goal is to change how some of those test cases are handled. Please
also include new/updated tests that exercise your proposed changes.
.rs
file focusing on the specific thing you want
to improve. Test verusfmt
on your reduced example to make sure the issue
still manifests; running with --check
is very helpful with this process.
The smaller your example file, the easier subsequent steps will be.--check
, you can also add -dd
to see 2nd level debug
output, which includes the parse tree. Look through the parse tree and
identify rules relevant to your particular example.src/verus.pest
to find the relevant rule(s) and see if the
grammar needs to be fixed or improved.to_doc
function in src/lib.rs
. This might be a
bit difficult to understand immediately, so having the pretty docs handy
is quite helpful. Also, it is helpful to look at the relevant debug print
(using -d
or -dd
), which gives a serialized version of the recursively
expanded doc
, right before it has been optimized, so figuring out which
particular bit of it is not behaving as you like is quite helpful.comma_delimited_exprs_for_verus_clauses
and
groupable_comma_delimited_exprs_for_verus_clauses
, for example.In general, we try to adhere to Rust's style guide. Tests for such adherence live in
tests/rustfmt-matching.rs. These tests will compare the output
of rustfmt
to that of verusfmt
. You can run them via cargo test
.
In various places, we deviate from Rust's style, either to simplify the formatter or to handle Verus-specific syntax. Tests for formatting such code live in tests/verus-consistency.rs. You can add a new test or modify an existing one by writing/changing the input code. The test's correct answer is maintained via the Insta testing framework.
Insta recommends installing the cargo-insta
tool for an improved review experience:
cargo install cargo-insta
You can run the tests normally with cargo test
, but it's often more convenient
to run the tests and review the results via:
cargo insta test
cargo insta review
or more succinctly:
cargo insta test --review