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}