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}