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