Skip to main content

cranelift_srcgen/
lib.rs

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