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::fs;
9use std::io::Write;
10
11pub mod error;
12
13static SHIFTWIDTH: usize = 4;
14
15/// A macro for constructing a [`FileLocation`] at the current location.
16#[macro_export]
17macro_rules! loc {
18    () => {
19        $crate::FileLocation::new(file!(), line!())
20    };
21}
22
23/// Record a source location; preferably, use [`loc`] directly.
24pub struct FileLocation {
25    file: &'static str,
26    line: u32,
27}
28
29impl FileLocation {
30    pub fn new(file: &'static str, line: u32) -> Self {
31        Self { file, line }
32    }
33}
34
35impl 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]
44macro_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)]
64pub enum Language {
65    Rust,
66    Isle,
67}
68
69impl 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.
88pub struct Formatter {
89    indent: usize,
90    lines: Vec<String>,
91    lang: Language,
92}
93
94impl 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.
253fn _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.
265fn 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.
325pub 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
332impl 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)]
383mod 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#"
412match 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#"
443match 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}