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 {
new(file: &'static str, line: u32) -> Self30 pub fn new(file: &'static str, line: u32) -> Self {
31 Self { file, line }
32 }
33 }
34
35 impl core::fmt::Display for FileLocation {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result36 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.
should_append_location(&self, line: &str) -> bool71 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.
comment_token(&self) -> &'static str79 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.
new(lang: Language) -> Self97 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.
indent_push(&mut self)106 pub fn indent_push(&mut self) {
107 self.indent += 1;
108 }
109
110 /// Decrease indentation by one level.
indent_pop(&mut self)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`.
indent<T, F: FnOnce(&mut Formatter) -> T>(&mut self, f: F) -> T117 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.
get_indent(&self) -> String125 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.
line(&mut self, contents: impl AsRef<str>)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).
line_with_location(&mut self, contents: impl AsRef<str>, location: FileLocation)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.
empty_line(&mut self)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.
multi_line(&mut self, s: &str)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.
comment(&mut self, s: impl AsRef<str>)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.
doc_comment(&mut self, contents: impl AsRef<str>)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.
add_block<T, F: FnOnce(&mut Formatter) -> T>(&mut self, start: &str, f: F) -> T187 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.
add_match(&mut self, m: Match)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.
write( &self, filename: impl AsRef<std::path::Path>, directory: &std::path::Path, ) -> Result<(), error::Error>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.
_indent(s: &str) -> Option<usize>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.
parse_multiline(s: &str) -> Vec<String>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`.
new(expr: impl Into<String>) -> Self334 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
set_catch_all(&mut self, clause: String)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.
arm<T: Into<String>, S: Into<String>>(&mut self, name: T, fields: Vec<S>, body: T)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.
arm_no_fields(&mut self, name: impl Into<String>, body: impl Into<String>)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
from_raw_string<S: Into<String>>(s: S) -> Vec<String>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]
adding_arms_works()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]
match_with_catchall_order()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]
parse_multiline_works()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]
formatter_basic_example_works()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]
get_indent_works()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]
fmt_can_add_type_to_lines()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]
fmt_can_add_indented_line()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]
fmt_can_add_doc_comments()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]
fmt_can_add_doc_comments_with_empty_lines()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