xref: /wasmtime-44.0.1/cranelift/srcgen/src/lib.rs (revision efb4e4c0)
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 {
30     pub fn new(file: &'static str, line: u32) -> Self {
31         Self { file, line }
32     }
33 }
34 
35 impl core::fmt::Display for FileLocation {
36     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.
71     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.
79     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.
97     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.
106     pub fn indent_push(&mut self) {
107         self.indent += 1;
108     }
109 
110     /// Decrease indentation by one level.
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`.
117     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.
125     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.
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).
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.
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.
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.
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.
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.
187     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.
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.
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.
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.
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`.
334     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 
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.
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.
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::parse_multiline;
385     use super::Formatter;
386     use super::Language;
387     use super::Match;
388 
389     fn from_raw_string<S: Into<String>>(s: S) -> Vec<String> {
390         s.into()
391             .trim()
392             .split("\n")
393             .into_iter()
394             .map(|x| format!("{x}\n"))
395             .collect()
396     }
397 
398     #[test]
399     fn adding_arms_works() {
400         let mut m = Match::new("x");
401         m.arm("Orange", vec!["a", "b"], "some body");
402         m.arm("Yellow", vec!["a", "b"], "some body");
403         m.arm("Green", vec!["a", "b"], "different body");
404         m.arm("Blue", vec!["x", "y"], "some body");
405         assert_eq!(m.arms.len(), 3);
406 
407         let mut fmt = Formatter::new(Language::Rust);
408         fmt.add_match(m);
409 
410         let expected_lines = from_raw_string(
411             r#"
412 match x {
413     Green { a, b } => {
414         different body
415     }
416     Orange { a, b } |
417     Yellow { a, b } => {
418         some body
419     }
420     Blue { x, y } => {
421         some body
422     }
423 }
424         "#,
425         );
426         assert_eq!(fmt.lines, expected_lines);
427     }
428 
429     #[test]
430     fn match_with_catchall_order() {
431         // The catchall placeholder must be placed after other clauses.
432         let mut m = Match::new("x");
433         m.arm("Orange", vec!["a", "b"], "some body");
434         m.arm("Green", vec!["a", "b"], "different body");
435         m.arm_no_fields("_", "unreachable!()");
436         assert_eq!(m.arms.len(), 2); // catchall is not counted
437 
438         let mut fmt = Formatter::new(Language::Rust);
439         fmt.add_match(m);
440 
441         let expected_lines = from_raw_string(
442             r#"
443 match x {
444     Green { a, b } => {
445         different body
446     }
447     Orange { a, b } => {
448         some body
449     }
450     _ => {
451         unreachable!()
452     }
453 }
454         "#,
455         );
456         assert_eq!(fmt.lines, expected_lines);
457     }
458 
459     #[test]
460     fn parse_multiline_works() {
461         let input = "\n    hello\n    world\n";
462         let expected = vec!["hello", "world"];
463         let output = parse_multiline(input);
464         assert_eq!(output, expected);
465     }
466 
467     #[test]
468     fn formatter_basic_example_works() {
469         let mut fmt = Formatter::new(Language::Rust);
470         fmt.line("Hello line 1");
471         fmt.indent_push();
472         fmt.comment("Nested comment");
473         fmt.indent_pop();
474         fmt.line("Back home again");
475         let expected_lines = vec![
476             "Hello line 1\n",
477             "    // Nested comment\n",
478             "Back home again\n",
479         ];
480         assert_eq!(fmt.lines, expected_lines);
481     }
482 
483     #[test]
484     fn get_indent_works() {
485         let mut fmt = Formatter::new(Language::Rust);
486         let expected_results = vec!["", "    ", "        ", ""];
487 
488         let actual_results = Vec::with_capacity(4);
489         (0..3).for_each(|_| {
490             fmt.get_indent();
491             fmt.indent_push();
492         });
493         (0..3).for_each(|_| fmt.indent_pop());
494         fmt.get_indent();
495 
496         actual_results
497             .into_iter()
498             .zip(expected_results.into_iter())
499             .for_each(|(actual, expected): (String, &str)| assert_eq!(&actual, expected));
500     }
501 
502     #[test]
503     fn fmt_can_add_type_to_lines() {
504         let mut fmt = Formatter::new(Language::Rust);
505         fmt.line(format!("pub const {}: Type = Type({:#x});", "example", 0));
506         let expected_lines = vec!["pub const example: Type = Type(0x0);\n"];
507         assert_eq!(fmt.lines, expected_lines);
508     }
509 
510     #[test]
511     fn fmt_can_add_indented_line() {
512         let mut fmt = Formatter::new(Language::Rust);
513         fmt.line("hello");
514         fmt.indent_push();
515         fmt.line("world");
516         let expected_lines = vec!["hello\n", "    world\n"];
517         assert_eq!(fmt.lines, expected_lines);
518     }
519 
520     #[test]
521     fn fmt_can_add_doc_comments() {
522         let mut fmt = Formatter::new(Language::Rust);
523         fmt.doc_comment("documentation\nis\ngood");
524         let expected_lines = vec!["/// documentation\n", "/// is\n", "/// good\n"];
525         assert_eq!(fmt.lines, expected_lines);
526     }
527 
528     #[test]
529     fn fmt_can_add_doc_comments_with_empty_lines() {
530         let mut fmt = Formatter::new(Language::Rust);
531         fmt.doc_comment(
532             r#"documentation
533         can be really good.
534 
535         If you stick to writing it.
536 "#,
537         );
538         let expected_lines = from_raw_string(
539             r#"
540 /// documentation
541 /// can be really good.
542 ///
543 /// If you stick to writing it."#,
544         );
545         assert_eq!(fmt.lines, expected_lines);
546     }
547 }
548