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