xref: /wasmtime-44.0.1/cranelift/srcgen/src/lib.rs (revision 703871a2)
1 //! A source code generator.
2 //!
3 //! This crate contains generic helper routines and classes for generating
4 //! source code.
5 
6 use std::cmp;
7 use std::collections::{BTreeMap, BTreeSet};
8 use std::fs;
9 use std::io::Write;
10 
11 pub mod error;
12 
13 static SHIFTWIDTH: usize = 4;
14 
15 /// A macro for constructing a [`FileLocation`] at the current location.
16 #[macro_export]
17 macro_rules! loc {
18     () => {
19         $crate::FileLocation::new(file!(), line!())
20     };
21 }
22 
23 /// Record a source location; preferably, use [`loc`] directly.
24 pub struct FileLocation {
25     file: &'static str,
26     line: u32,
27 }
28 
29 impl FileLocation {
new(file: &'static str, line: u32) -> Self30     pub fn new(file: &'static str, line: u32) -> Self {
31         Self { file, line }
32     }
33 }
34 
35 impl core::fmt::Display for FileLocation {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result36     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37         write!(f, "{}:{}", self.file, self.line)
38     }
39 }
40 
41 /// A macro that simplifies the usage of the [`Formatter`] by allowing format
42 /// strings.
43 #[macro_export]
44 macro_rules! fmtln {
45     ($fmt:ident, $fmtstring:expr, $($fmtargs:expr),*) => {
46         $fmt.line_with_location(format!($fmtstring, $($fmtargs),*), $crate::loc!())
47     };
48 
49     ($fmt:ident, $arg:expr) => {
50         $fmt.line_with_location(format!($arg), $crate::loc!())
51     };
52 
53     ($_:tt, $($args:expr),+) => {
54         compile_error!("This macro requires at least two arguments: the Formatter instance and a format string.")
55     };
56 
57     ($_:tt) => {
58         compile_error!("This macro requires at least two arguments: the Formatter instance and a format string.")
59     };
60 }
61 
62 /// Identify the source code language a [`Formatter`] will emit.
63 #[derive(Debug, Clone, Copy)]
64 pub enum Language {
65     Rust,
66     Isle,
67 }
68 
69 impl Language {
70     /// Determine if a [`FileLocation`] comment should be appended to a line.
should_append_location(&self, line: &str) -> bool71     pub fn should_append_location(&self, line: &str) -> bool {
72         match self {
73             Language::Rust => !line.ends_with(['{', '}']),
74             Language::Isle => true,
75         }
76     }
77 
78     /// Get the comment token for the language.
comment_token(&self) -> &'static str79     pub fn comment_token(&self) -> &'static str {
80         match self {
81             Language::Rust => "//",
82             Language::Isle => ";;",
83         }
84     }
85 }
86 
87 /// Collect source code to be written to a file and keep track of indentation.
88 pub struct Formatter {
89     indent: usize,
90     lines: Vec<String>,
91     lang: Language,
92 }
93 
94 impl Formatter {
95     /// Source code formatter class. Used to collect source code of a specific
96     /// [`Language`] to be written to a file, and keep track of indentation.
new(lang: Language) -> Self97     pub fn new(lang: Language) -> Self {
98         Self {
99             indent: 0,
100             lines: Vec::new(),
101             lang,
102         }
103     }
104 
105     /// Increase current indentation level by one.
indent_push(&mut self)106     pub fn indent_push(&mut self) {
107         self.indent += 1;
108     }
109 
110     /// Decrease indentation by one level.
indent_pop(&mut self)111     pub fn indent_pop(&mut self) {
112         assert!(self.indent > 0, "Already at top level indentation");
113         self.indent -= 1;
114     }
115 
116     /// Increase indentation level for the duration of `f`.
indent<T, F: FnOnce(&mut Formatter) -> T>(&mut self, f: F) -> T117     pub fn indent<T, F: FnOnce(&mut Formatter) -> T>(&mut self, f: F) -> T {
118         self.indent_push();
119         let ret = f(self);
120         self.indent_pop();
121         ret
122     }
123 
124     /// Get the current whitespace indentation in the form of a String.
get_indent(&self) -> String125     fn get_indent(&self) -> String {
126         if self.indent == 0 {
127             String::new()
128         } else {
129             format!("{:-1$}", " ", self.indent * SHIFTWIDTH)
130         }
131     }
132 
133     /// Add an indented line.
line(&mut self, contents: impl AsRef<str>)134     pub fn line(&mut self, contents: impl AsRef<str>) {
135         let indented_line = format!("{}{}\n", self.get_indent(), contents.as_ref());
136         self.lines.push(indented_line);
137     }
138 
139     /// Add an indented lin with a given a `location` appended as a comment to
140     /// the line (this is useful for identifying where a line was generated).
line_with_location(&mut self, contents: impl AsRef<str>, location: FileLocation)141     pub fn line_with_location(&mut self, contents: impl AsRef<str>, location: FileLocation) {
142         let indent = self.get_indent();
143         let contents = contents.as_ref();
144         let indented_line = if self.lang.should_append_location(contents) {
145             let comment_token = self.lang.comment_token();
146             format!("{indent}{contents} {comment_token} {location}\n")
147         } else {
148             format!("{indent}{contents}\n")
149         };
150         self.lines.push(indented_line);
151     }
152 
153     /// Pushes an empty line.
empty_line(&mut self)154     pub fn empty_line(&mut self) {
155         self.lines.push("\n".to_string());
156     }
157 
158     /// Add one or more lines after stripping common indentation.
multi_line(&mut self, s: &str)159     pub fn multi_line(&mut self, s: &str) {
160         parse_multiline(s).into_iter().for_each(|l| self.line(&l));
161     }
162 
163     /// Add a comment line.
comment(&mut self, s: impl AsRef<str>)164     pub fn comment(&mut self, s: impl AsRef<str>) {
165         // Avoid `fmtln!` here: we don't want to append a location comment to a
166         // comment.
167         self.line(format!("{} {}", self.lang.comment_token(), s.as_ref()));
168     }
169 
170     /// Add a (multi-line) documentation comment.
doc_comment(&mut self, contents: impl AsRef<str>)171     pub fn doc_comment(&mut self, contents: impl AsRef<str>) {
172         assert!(matches!(self.lang, Language::Rust));
173         parse_multiline(contents.as_ref())
174             .iter()
175             .map(|l| {
176                 if l.is_empty() {
177                     "///".into()
178                 } else {
179                     format!("/// {l}")
180                 }
181             })
182             .for_each(|s| self.line(s.as_str()));
183     }
184 
185     /// Add a brace-delimited block that begins with `start`: i.e., `<start> {
186     /// <f()> }`. This properly indents the contents of the block.
add_block<T, F: FnOnce(&mut Formatter) -> T>(&mut self, start: &str, f: F) -> T187     pub fn add_block<T, F: FnOnce(&mut Formatter) -> T>(&mut self, start: &str, f: F) -> T {
188         assert!(matches!(self.lang, Language::Rust));
189         self.line(format!("{start} {{"));
190         let ret = self.indent(f);
191         self.line("}");
192         ret
193     }
194 
195     /// Add a match expression.
add_match(&mut self, m: Match)196     pub fn add_match(&mut self, m: Match) {
197         assert!(matches!(self.lang, Language::Rust));
198         fmtln!(self, "match {} {{", m.expr);
199         self.indent(|fmt| {
200             for (&(ref fields, ref body), ref names) in m.arms.iter() {
201                 // name { fields } | name { fields } => { body }
202                 let conditions = names
203                     .iter()
204                     .map(|name| {
205                         if !fields.is_empty() {
206                             format!("{} {{ {} }}", name, fields.join(", "))
207                         } else {
208                             name.clone()
209                         }
210                     })
211                     .collect::<Vec<_>>()
212                     .join(" |\n")
213                     + " => {";
214 
215                 fmt.multi_line(&conditions);
216                 fmt.indent(|fmt| {
217                     fmt.line(body);
218                 });
219                 fmt.line("}");
220             }
221 
222             // Make sure to include the catch all clause last.
223             if let Some(body) = m.catch_all {
224                 fmt.line("_ => {");
225                 fmt.indent(|fmt| {
226                     fmt.line(body);
227                 });
228                 fmt.line("}");
229             }
230         });
231         self.line("}");
232     }
233 
234     /// Write `self.lines` to a file.
write( &self, filename: impl AsRef<std::path::Path>, directory: &std::path::Path, ) -> Result<(), error::Error>235     pub fn write(
236         &self,
237         filename: impl AsRef<std::path::Path>,
238         directory: &std::path::Path,
239     ) -> Result<(), error::Error> {
240         let path = directory.join(&filename);
241         eprintln!("Writing generated file: {}", path.display());
242         let mut f = fs::File::create(path)?;
243 
244         for l in self.lines.iter().map(|l| l.as_bytes()) {
245             f.write_all(l)?;
246         }
247 
248         Ok(())
249     }
250 }
251 
252 /// Compute the indentation of s, or None of an empty line.
_indent(s: &str) -> Option<usize>253 fn _indent(s: &str) -> Option<usize> {
254     if s.is_empty() {
255         None
256     } else {
257         let t = s.trim_start();
258         Some(s.len() - t.len())
259     }
260 }
261 
262 /// Given a multi-line string, split it into a sequence of lines after
263 /// stripping a common indentation. This is useful for strings defined with
264 /// doc strings.
parse_multiline(s: &str) -> Vec<String>265 fn parse_multiline(s: &str) -> Vec<String> {
266     // Convert tabs into spaces.
267     let expanded_tab = format!("{:-1$}", " ", SHIFTWIDTH);
268     let lines: Vec<String> = s.lines().map(|l| l.replace('\t', &expanded_tab)).collect();
269 
270     // Determine minimum indentation, ignoring the first line and empty lines.
271     let indent = lines
272         .iter()
273         .skip(1)
274         .filter(|l| !l.trim().is_empty())
275         .map(|l| l.len() - l.trim_start().len())
276         .min();
277 
278     // Strip off leading blank lines.
279     let mut lines_iter = lines.iter().skip_while(|l| l.is_empty());
280     let mut trimmed = Vec::with_capacity(lines.len());
281 
282     // Remove indentation (first line is special)
283     if let Some(s) = lines_iter.next().map(|l| l.trim()).map(|l| l.to_string()) {
284         trimmed.push(s);
285     }
286 
287     // Remove trailing whitespace from other lines.
288     let mut other_lines = if let Some(indent) = indent {
289         // Note that empty lines may have fewer than `indent` chars.
290         lines_iter
291             .map(|l| &l[cmp::min(indent, l.len())..])
292             .map(|l| l.trim_end())
293             .map(|l| l.to_string())
294             .collect::<Vec<_>>()
295     } else {
296         lines_iter
297             .map(|l| l.trim_end())
298             .map(|l| l.to_string())
299             .collect::<Vec<_>>()
300     };
301 
302     trimmed.append(&mut other_lines);
303 
304     // Strip off trailing blank lines.
305     while let Some(s) = trimmed.pop() {
306         if s.is_empty() {
307             continue;
308         } else {
309             trimmed.push(s);
310             break;
311         }
312     }
313 
314     trimmed
315 }
316 
317 /// Match formatting class.
318 ///
319 /// Match objects collect all the information needed to emit a Rust `match`
320 /// expression, automatically deduplicating overlapping identical arms.
321 ///
322 /// Note that this class is ignorant of Rust types, and considers two fields
323 /// with the same name to be equivalent. BTreeMap/BTreeSet are used to
324 /// represent the arms in order to make the order deterministic.
325 pub struct Match {
326     expr: String,
327     arms: BTreeMap<(Vec<String>, String), BTreeSet<String>>,
328     /// The clause for the placeholder pattern _.
329     catch_all: Option<String>,
330 }
331 
332 impl Match {
333     /// Create a new match statement on `expr`.
new(expr: impl Into<String>) -> Self334     pub fn new(expr: impl Into<String>) -> Self {
335         Self {
336             expr: expr.into(),
337             arms: BTreeMap::new(),
338             catch_all: None,
339         }
340     }
341 
set_catch_all(&mut self, clause: String)342     fn set_catch_all(&mut self, clause: String) {
343         assert!(self.catch_all.is_none());
344         self.catch_all = Some(clause);
345     }
346 
347     /// Add an arm that reads fields to the Match statement.
arm<T: Into<String>, S: Into<String>>(&mut self, name: T, fields: Vec<S>, body: T)348     pub fn arm<T: Into<String>, S: Into<String>>(&mut self, name: T, fields: Vec<S>, body: T) {
349         let name = name.into();
350         assert!(
351             name != "_",
352             "catch all clause can't extract fields, use arm_no_fields instead."
353         );
354 
355         let body = body.into();
356         let fields = fields.into_iter().map(|x| x.into()).collect();
357         let match_arm = self
358             .arms
359             .entry((fields, body))
360             .or_insert_with(BTreeSet::new);
361         match_arm.insert(name);
362     }
363 
364     /// Adds an arm that doesn't read anythings from the fields to the Match statement.
arm_no_fields(&mut self, name: impl Into<String>, body: impl Into<String>)365     pub fn arm_no_fields(&mut self, name: impl Into<String>, body: impl Into<String>) {
366         let body = body.into();
367 
368         let name = name.into();
369         if name == "_" {
370             self.set_catch_all(body);
371             return;
372         }
373 
374         let match_arm = self
375             .arms
376             .entry((Vec::new(), body))
377             .or_insert_with(BTreeSet::new);
378         match_arm.insert(name);
379     }
380 }
381 
382 #[cfg(test)]
383 mod srcgen_tests {
384     use super::Formatter;
385     use super::Language;
386     use super::Match;
387     use super::parse_multiline;
388 
from_raw_string<S: Into<String>>(s: S) -> Vec<String>389     fn from_raw_string<S: Into<String>>(s: S) -> Vec<String> {
390         s.into()
391             .trim()
392             .split("\n")
393             .map(|x| format!("{x}\n"))
394             .collect()
395     }
396 
397     #[test]
adding_arms_works()398     fn adding_arms_works() {
399         let mut m = Match::new("x");
400         m.arm("Orange", vec!["a", "b"], "some body");
401         m.arm("Yellow", vec!["a", "b"], "some body");
402         m.arm("Green", vec!["a", "b"], "different body");
403         m.arm("Blue", vec!["x", "y"], "some body");
404         assert_eq!(m.arms.len(), 3);
405 
406         let mut fmt = Formatter::new(Language::Rust);
407         fmt.add_match(m);
408 
409         let expected_lines = from_raw_string(
410             r#"
411 match x {
412     Green { a, b } => {
413         different body
414     }
415     Orange { a, b } |
416     Yellow { a, b } => {
417         some body
418     }
419     Blue { x, y } => {
420         some body
421     }
422 }
423         "#,
424         );
425         assert_eq!(fmt.lines, expected_lines);
426     }
427 
428     #[test]
match_with_catchall_order()429     fn match_with_catchall_order() {
430         // The catchall placeholder must be placed after other clauses.
431         let mut m = Match::new("x");
432         m.arm("Orange", vec!["a", "b"], "some body");
433         m.arm("Green", vec!["a", "b"], "different body");
434         m.arm_no_fields("_", "unreachable!()");
435         assert_eq!(m.arms.len(), 2); // catchall is not counted
436 
437         let mut fmt = Formatter::new(Language::Rust);
438         fmt.add_match(m);
439 
440         let expected_lines = from_raw_string(
441             r#"
442 match x {
443     Green { a, b } => {
444         different body
445     }
446     Orange { a, b } => {
447         some body
448     }
449     _ => {
450         unreachable!()
451     }
452 }
453         "#,
454         );
455         assert_eq!(fmt.lines, expected_lines);
456     }
457 
458     #[test]
parse_multiline_works()459     fn parse_multiline_works() {
460         let input = "\n    hello\n    world\n";
461         let expected = vec!["hello", "world"];
462         let output = parse_multiline(input);
463         assert_eq!(output, expected);
464     }
465 
466     #[test]
formatter_basic_example_works()467     fn formatter_basic_example_works() {
468         let mut fmt = Formatter::new(Language::Rust);
469         fmt.line("Hello line 1");
470         fmt.indent_push();
471         fmt.comment("Nested comment");
472         fmt.indent_pop();
473         fmt.line("Back home again");
474         let expected_lines = vec![
475             "Hello line 1\n",
476             "    // Nested comment\n",
477             "Back home again\n",
478         ];
479         assert_eq!(fmt.lines, expected_lines);
480     }
481 
482     #[test]
get_indent_works()483     fn get_indent_works() {
484         let mut fmt = Formatter::new(Language::Rust);
485         let expected_results = vec!["", "    ", "        ", ""];
486 
487         let actual_results = Vec::with_capacity(4);
488         (0..3).for_each(|_| {
489             fmt.get_indent();
490             fmt.indent_push();
491         });
492         (0..3).for_each(|_| fmt.indent_pop());
493         fmt.get_indent();
494 
495         actual_results
496             .into_iter()
497             .zip(expected_results)
498             .for_each(|(actual, expected): (String, &str)| assert_eq!(&actual, expected));
499     }
500 
501     #[test]
fmt_can_add_type_to_lines()502     fn fmt_can_add_type_to_lines() {
503         let mut fmt = Formatter::new(Language::Rust);
504         fmt.line(format!("pub const {}: Type = Type({:#x});", "example", 0));
505         let expected_lines = vec!["pub const example: Type = Type(0x0);\n"];
506         assert_eq!(fmt.lines, expected_lines);
507     }
508 
509     #[test]
fmt_can_add_indented_line()510     fn fmt_can_add_indented_line() {
511         let mut fmt = Formatter::new(Language::Rust);
512         fmt.line("hello");
513         fmt.indent_push();
514         fmt.line("world");
515         let expected_lines = vec!["hello\n", "    world\n"];
516         assert_eq!(fmt.lines, expected_lines);
517     }
518 
519     #[test]
fmt_can_add_doc_comments()520     fn fmt_can_add_doc_comments() {
521         let mut fmt = Formatter::new(Language::Rust);
522         fmt.doc_comment("documentation\nis\ngood");
523         let expected_lines = vec!["/// documentation\n", "/// is\n", "/// good\n"];
524         assert_eq!(fmt.lines, expected_lines);
525     }
526 
527     #[test]
fmt_can_add_doc_comments_with_empty_lines()528     fn fmt_can_add_doc_comments_with_empty_lines() {
529         let mut fmt = Formatter::new(Language::Rust);
530         fmt.doc_comment(
531             r#"documentation
532         can be really good.
533 
534         If you stick to writing it.
535 "#,
536         );
537         let expected_lines = from_raw_string(
538             r#"
539 /// documentation
540 /// can be really good.
541 ///
542 /// If you stick to writing it."#,
543         );
544         assert_eq!(fmt.lines, expected_lines);
545     }
546 }
547