Skip to main content

rdlfmt/
lib.rs

1//! A formatter for SystemRDL.
2//!
3//! ```text
4//! source text
5//!     |
6//!     v  syntax              lossless CST, comments and all
7//!     v  rules               one function per node kind
8//!     v  formatter           annotated output and alignment
9//!     v  String              final rendering
10//! ```
11//!
12//! # A deliberately small intermediate representation
13//!
14//! Pretty-printers usually build a document IR (Wadler groups, Oppen's
15//! algorithm) because their layout decisions depend on rendered width: whether
16//! a list fits on one line cannot be known until everything inside it has been
17//! laid out, so the decision has to be deferred and the alternatives measured.
18//!
19//! None of the *line breaking* rules here are width-dependent. Following the PeakRDL style
20//! guide, braces always break, statements are one per line, expressions never
21//! break, and a parenthesised list breaks when it holds more than one element.
22//! Every one of those is decidable from the tree alone, before a single
23//! character is written, so this formatter does not need groups, alternatives,
24//! or a fitting algorithm.
25//!
26//! Column alignment does need hindsight. The formatter therefore retains a
27//! narrow IR over its ordinary output: semantic row and cell boundaries grouped
28//! into list-local scopes. Once every newline is final, an alignment pass
29//! measures adjacent one-line rows and inserts padding before the String is
30//! returned. Padding never feeds back into layout.
31//!
32//! # What the formatter will not do
33//!
34//! Reformat a file the parser did not fully understand. [`format()`] returns
35//! [`FormatError`] when the parse reports errors, because the rules assume a
36//! tree shape that error recovery does not guarantee, and rewriting a file
37//! whose structure was guessed at is how a formatter corrupts code.
38//!
39//! Preprocessor directives need no separate rule against them, which is the
40//! point of treating even the conditionals as trivia: a `` `ifdef `` whose
41//! branches hand a brace back and forth leaves the braces unbalanced, and so is
42//! refused by the same check as any other input the parser could not follow.
43//! Everything else formats like a comment -- its own line, payload untouched --
44//! except that a branching directive is left-aligned rather than indented with
45//! the code around it, having no place in the brace hierarchy. See the docs in
46//! [`crate::syntax::parser`] for why ignoring a conditional cannot corrupt
47//! the file.
48
49pub mod syntax;
50
51mod formatter;
52mod rules;
53
54use crate::syntax::{ParseError, SyntaxKind, lex, parse};
55use formatter::{Formatter, line_ending};
56
57/// Why no formatted output was produced.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum FormatError {
60    /// The input did not parse. Formatting is refused rather than attempted:
61    /// the rules assume a tree shape that error recovery does not guarantee.
62    Parse(Vec<ParseError>),
63    /// Formatting would have changed the code, not just its layout.
64    ///
65    /// Always a bug in this crate, never something the input can cause. The
66    /// output is withheld so that the bug cannot reach a file.
67    Corrupted(String),
68}
69
70impl FormatError {
71    /// The parse errors that caused the refusal, or empty for other causes.
72    pub fn errors(&self) -> &[ParseError] {
73        match self {
74            FormatError::Parse(errors) => errors,
75            FormatError::Corrupted(_) => &[],
76        }
77    }
78}
79
80impl std::fmt::Display for FormatError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            FormatError::Parse(errors) => {
84                write!(f, "cannot format input with syntax errors: ")?;
85                for (i, err) in errors.iter().enumerate() {
86                    if i > 0 {
87                        write!(f, "; ")?;
88                    }
89                    write!(f, "{err}")?;
90                }
91                Ok(())
92            }
93            FormatError::Corrupted(what) => write!(
94                f,
95                "internal error: formatting would have changed the code ({what}); \
96                 this is a bug in rdlfmt, please report it"
97            ),
98        }
99    }
100}
101
102impl std::error::Error for FormatError {}
103
104/// Formats SystemRDL source.
105///
106/// There is nothing to configure, deliberately: a formatter earns its value by
107/// ending arguments, not by relocating them into a config file. Indentation is
108/// four spaces, which is what the PeakRDL style guide asks for.
109///
110/// A `// rdlfmt: off` comment suppresses formatting for the statements that
111/// follow it, until a `// rdlfmt: on` or the end of the enclosing body; a
112/// `// rdlfmt: skip` covers the single statement below it. That is an opt-out
113/// for one passage rather than a setting -- it travels with the code it
114/// applies to, and says only that a passage is already the way its author
115/// wants it.
116///
117/// The output is verified before it is returned: see `verify`. A caller that
118/// gets `Ok` has a guarantee, not just a hope, that only whitespace moved.
119///
120/// # Errors
121/// [`FormatError::Parse`] if `src` does not parse cleanly. The source is left
122/// for the caller to report on rather than being passed through unchanged, so
123/// that a broken file is never silently mistaken for a formatted one.
124///
125/// [`FormatError::Corrupted`] if the formatter has a bug.
126pub fn format(src: &str) -> Result<String, FormatError> {
127    let parsed = parse(src);
128    if !parsed.errors().is_empty() {
129        return Err(FormatError::Parse(parsed.errors().to_vec()));
130    }
131
132    let mut f = Formatter::new(src);
133    rules::format_node(&mut f, &parsed.syntax());
134    let out = f.finish();
135
136    verify(src, &out)?;
137    Ok(out)
138}
139
140/// Checks that formatting moved nothing but whitespace.
141///
142/// The test suite asserts this over the inputs someone thought to write down.
143/// Doing it here instead makes it hold for every input there will ever be,
144/// which is what justifies a tool that overwrites source files by default. The
145/// cost is one extra lex of the output -- nothing next to the parse that
146/// produced it.
147///
148/// Comments are compared alongside the code, trimmed at the end, because a
149/// dropped comment is a real loss even though it changes no behaviour. The
150/// trim is what lets the formatter tidy trailing spaces inside one.
151fn verify(src: &str, out: &str) -> Result<(), FormatError> {
152    // Checked separately because the token comparison below cannot see it: line
153    // endings live inside the whitespace this function filters out, so silently
154    // rewriting every one of them would pass the check that follows.
155    let (want, got) = (line_ending(src), line_ending(out));
156    if want != got {
157        return Err(FormatError::Corrupted(format!(
158            "line endings changed from {want:?} to {got:?}"
159        )));
160    }
161
162    let (before, after) = (lex(src), lex(out));
163    let keep = |(kind, _): &(SyntaxKind, &str)| *kind != SyntaxKind::WHITESPACE;
164    let mut before = before.iter().filter(keep);
165    let mut after = after.iter().filter(keep);
166
167    loop {
168        return match (before.next(), after.next()) {
169            (None, None) => Ok(()),
170            (Some((a, at)), Some((b, bt))) if a == b && at.trim_end() == bt.trim_end() => continue,
171            (Some((a, at)), Some((b, bt))) => Err(FormatError::Corrupted(format!(
172                "{a:?} {at:?} became {b:?} {bt:?}"
173            ))),
174            (Some((a, at)), None) => Err(FormatError::Corrupted(format!("{a:?} {at:?} was lost"))),
175            (None, Some((b, bt))) => Err(FormatError::Corrupted(format!(
176                "{b:?} {bt:?} appeared from nowhere"
177            ))),
178        };
179    }
180}