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::Formatter; 385 use super::Language; 386 use super::Match; 387 use super::parse_multiline; 388 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] 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] 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] 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] 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] 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] 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] 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] 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] 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