//! Source code generator. //! //! The `srcgen` module contains generic helper routines and classes for //! generating source code. #![macro_use] use std::cmp; use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::io::Write; use std::path; use crate::error; static SHIFTWIDTH: usize = 4; /// A macro that simplifies the usage of the Formatter by allowing format /// strings. macro_rules! fmtln { ($fmt:ident, $fmtstring:expr, $($fmtargs:expr),*) => { $fmt.line(format!($fmtstring, $($fmtargs),*)); }; ($fmt:ident, $arg:expr) => { $fmt.line($arg); }; ($_:tt, $($args:expr),+) => { compile_error!("This macro requires at least two arguments: the Formatter instance and a format string."); }; ($_:tt) => { compile_error!("This macro requires at least two arguments: the Formatter instance and a format string."); }; } pub(crate) struct Formatter { indent: usize, lines: Vec, } impl Formatter { /// Source code formatter class. Used to collect source code to be written /// to a file, and keep track of indentation. pub fn new() -> Self { Self { indent: 0, lines: Vec::new(), } } /// Increase current indentation level by one. pub fn indent_push(&mut self) { self.indent += 1; } /// Decrease indentation by one level. pub fn indent_pop(&mut self) { assert!(self.indent > 0, "Already at top level indentation"); self.indent -= 1; } pub fn indent T>(&mut self, f: F) -> T { self.indent_push(); let ret = f(self); self.indent_pop(); ret } /// Get the current whitespace indentation in the form of a String. fn get_indent(&self) -> String { if self.indent == 0 { String::new() } else { format!("{:-1$}", " ", self.indent * SHIFTWIDTH) } } /// Get a string containing whitespace outdented one level. Used for /// lines of code that are inside a single indented block. fn get_outdent(&mut self) -> String { self.indent_pop(); let s = self.get_indent(); self.indent_push(); s } /// Add an indented line. pub fn line(&mut self, contents: impl AsRef) { let indented_line = format!("{}{}\n", self.get_indent(), contents.as_ref()); self.lines.push(indented_line); } /// Pushes an empty line. pub fn empty_line(&mut self) { self.lines.push("\n".to_string()); } /// Emit a line outdented one level. pub fn outdented_line(&mut self, s: &str) { let new_line = format!("{}{}\n", self.get_outdent(), s); self.lines.push(new_line); } /// Write `self.lines` to a file. pub fn update_file( &self, filename: impl AsRef, directory: &str, ) -> Result<(), error::Error> { #[cfg(target_family = "windows")] let path_str = format!("{}\\{}", directory, filename.as_ref()); #[cfg(not(target_family = "windows"))] let path_str = format!("{}/{}", directory, filename.as_ref()); let path = path::Path::new(&path_str); let mut f = fs::File::create(path)?; for l in self.lines.iter().map(|l| l.as_bytes()) { f.write_all(l)?; } Ok(()) } /// Add one or more lines after stripping common indentation. pub fn multi_line(&mut self, s: &str) { parse_multiline(s).into_iter().for_each(|l| self.line(&l)); } /// Add a comment line. pub fn comment(&mut self, s: impl AsRef) { fmtln!(self, "// {}", s.as_ref()); } /// Add a (multi-line) documentation comment. pub fn doc_comment(&mut self, contents: impl AsRef) { parse_multiline(contents.as_ref()) .iter() .map(|l| { if l.is_empty() { "///".into() } else { format!("/// {}", l) } }) .for_each(|s| self.line(s.as_str())); } /// Add a match expression. pub fn add_match(&mut self, m: Match) { fmtln!(self, "match {} {{", m.expr); self.indent(|fmt| { for (&(ref fields, ref body), ref names) in m.arms.iter() { // name { fields } | name { fields } => { body } let conditions = names .iter() .map(|name| { if !fields.is_empty() { format!("{} {{ {} }}", name, fields.join(", ")) } else { name.clone() } }) .collect::>() .join(" |\n") + " => {"; fmt.multi_line(&conditions); fmt.indent(|fmt| { fmt.line(body); }); fmt.line("}"); } // Make sure to include the catch all clause last. if let Some(body) = m.catch_all { fmt.line("_ => {"); fmt.indent(|fmt| { fmt.line(body); }); fmt.line("}"); } }); self.line("}"); } } /// Compute the indentation of s, or None of an empty line. fn _indent(s: &str) -> Option { if s.is_empty() { None } else { let t = s.trim_start(); Some(s.len() - t.len()) } } /// Given a multi-line string, split it into a sequence of lines after /// stripping a common indentation. This is useful for strings defined with /// doc strings. fn parse_multiline(s: &str) -> Vec { // Convert tabs into spaces. let expanded_tab = format!("{:-1$}", " ", SHIFTWIDTH); let lines: Vec = s.lines().map(|l| l.replace("\t", &expanded_tab)).collect(); // Determine minimum indentation, ignoring the first line and empty lines. let indent = lines .iter() .skip(1) .filter(|l| !l.trim().is_empty()) .map(|l| l.len() - l.trim_start().len()) .min(); // Strip off leading blank lines. let mut lines_iter = lines.iter().skip_while(|l| l.is_empty()); let mut trimmed = Vec::with_capacity(lines.len()); // Remove indentation (first line is special) if let Some(s) = lines_iter.next().map(|l| l.trim()).map(|l| l.to_string()) { trimmed.push(s); } // Remove trailing whitespace from other lines. let mut other_lines = if let Some(indent) = indent { // Note that empty lines may have fewer than `indent` chars. lines_iter .map(|l| &l[cmp::min(indent, l.len())..]) .map(|l| l.trim_end()) .map(|l| l.to_string()) .collect::>() } else { lines_iter .map(|l| l.trim_end()) .map(|l| l.to_string()) .collect::>() }; trimmed.append(&mut other_lines); // Strip off trailing blank lines. while let Some(s) = trimmed.pop() { if s.is_empty() { continue; } else { trimmed.push(s); break; } } trimmed } /// Match formatting class. /// /// Match objects collect all the information needed to emit a Rust `match` /// expression, automatically deduplicating overlapping identical arms. /// /// Note that this class is ignorant of Rust types, and considers two fields /// with the same name to be equivalent. BTreeMap/BTreeSet are used to /// represent the arms in order to make the order deterministic. pub(crate) struct Match { expr: String, arms: BTreeMap<(Vec, String), BTreeSet>, /// The clause for the placeholder pattern _. catch_all: Option, } impl Match { /// Create a new match statement on `expr`. pub fn new(expr: impl Into) -> Self { Self { expr: expr.into(), arms: BTreeMap::new(), catch_all: None, } } fn set_catch_all(&mut self, clause: String) { assert!(self.catch_all.is_none()); self.catch_all = Some(clause); } /// Add an arm that reads fields to the Match statement. pub fn arm, S: Into>(&mut self, name: T, fields: Vec, body: T) { let name = name.into(); assert!( name != "_", "catch all clause can't extract fields, use arm_no_fields instead." ); let body = body.into(); let fields = fields.into_iter().map(|x| x.into()).collect(); let match_arm = self .arms .entry((fields, body)) .or_insert_with(BTreeSet::new); match_arm.insert(name); } /// Adds an arm that doesn't read anythings from the fields to the Match statement. pub fn arm_no_fields(&mut self, name: impl Into, body: impl Into) { let body = body.into(); let name = name.into(); if name == "_" { self.set_catch_all(body); return; } let match_arm = self .arms .entry((Vec::new(), body)) .or_insert_with(BTreeSet::new); match_arm.insert(name); } } #[cfg(test)] mod srcgen_tests { use super::parse_multiline; use super::Formatter; use super::Match; fn from_raw_string>(s: S) -> Vec { s.into() .trim() .split("\n") .into_iter() .map(|x| format!("{}\n", x)) .collect() } #[test] fn adding_arms_works() { let mut m = Match::new("x"); m.arm("Orange", vec!["a", "b"], "some body"); m.arm("Yellow", vec!["a", "b"], "some body"); m.arm("Green", vec!["a", "b"], "different body"); m.arm("Blue", vec!["x", "y"], "some body"); assert_eq!(m.arms.len(), 3); let mut fmt = Formatter::new(); fmt.add_match(m); let expected_lines = from_raw_string( r#" match x { Green { a, b } => { different body } Orange { a, b } | Yellow { a, b } => { some body } Blue { x, y } => { some body } } "#, ); assert_eq!(fmt.lines, expected_lines); } #[test] fn match_with_catchall_order() { // The catchall placeholder must be placed after other clauses. let mut m = Match::new("x"); m.arm("Orange", vec!["a", "b"], "some body"); m.arm("Green", vec!["a", "b"], "different body"); m.arm_no_fields("_", "unreachable!()"); assert_eq!(m.arms.len(), 2); // catchall is not counted let mut fmt = Formatter::new(); fmt.add_match(m); let expected_lines = from_raw_string( r#" match x { Green { a, b } => { different body } Orange { a, b } => { some body } _ => { unreachable!() } } "#, ); assert_eq!(fmt.lines, expected_lines); } #[test] fn parse_multiline_works() { let input = "\n hello\n world\n"; let expected = vec!["hello", "world"]; let output = parse_multiline(input); assert_eq!(output, expected); } #[test] fn formatter_basic_example_works() { let mut fmt = Formatter::new(); fmt.line("Hello line 1"); fmt.indent_push(); fmt.comment("Nested comment"); fmt.indent_pop(); fmt.line("Back home again"); let expected_lines = vec![ "Hello line 1\n", " // Nested comment\n", "Back home again\n", ]; assert_eq!(fmt.lines, expected_lines); } #[test] fn get_indent_works() { let mut fmt = Formatter::new(); let expected_results = vec!["", " ", " ", ""]; let actual_results = Vec::with_capacity(4); (0..3).for_each(|_| { fmt.get_indent(); fmt.indent_push(); }); (0..3).for_each(|_| fmt.indent_pop()); fmt.get_indent(); actual_results .into_iter() .zip(expected_results.into_iter()) .for_each(|(actual, expected): (String, &str)| assert_eq!(&actual, expected)); } #[test] fn fmt_can_add_type_to_lines() { let mut fmt = Formatter::new(); fmt.line(format!("pub const {}: Type = Type({:#x});", "example", 0,)); let expected_lines = vec!["pub const example: Type = Type(0x0);\n"]; assert_eq!(fmt.lines, expected_lines); } #[test] fn fmt_can_add_indented_line() { let mut fmt = Formatter::new(); fmt.line("hello"); fmt.indent_push(); fmt.line("world"); let expected_lines = vec!["hello\n", " world\n"]; assert_eq!(fmt.lines, expected_lines); } #[test] fn fmt_can_add_doc_comments() { let mut fmt = Formatter::new(); fmt.doc_comment("documentation\nis\ngood"); let expected_lines = vec!["/// documentation\n", "/// is\n", "/// good\n"]; assert_eq!(fmt.lines, expected_lines); } #[test] fn fmt_can_add_doc_comments_with_empty_lines() { let mut fmt = Formatter::new(); fmt.doc_comment( r#"documentation can be really good. If you stick to writing it. "#, ); let expected_lines = from_raw_string( r#" /// documentation /// can be really good. /// /// If you stick to writing it."#, ); assert_eq!(fmt.lines, expected_lines); } }