1use 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#[macro_export]
17macro_rules! loc {
18 () => {
19 $crate::FileLocation::new(file!(), line!())
20 };
21}
22
23pub 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#[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#[derive(Debug, Clone, Copy)]
64pub enum Language {
65 Rust,
66 Isle,
67}
68
69impl Language {
70 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 pub fn comment_token(&self) -> &'static str {
80 match self {
81 Language::Rust => "//",
82 Language::Isle => ";;",
83 }
84 }
85}
86
87pub struct Formatter {
89 indent: usize,
90 lines: Vec<String>,
91 lang: Language,
92}
93
94impl Formatter {
95 pub fn new(lang: Language) -> Self {
98 Self {
99 indent: 0,
100 lines: Vec::new(),
101 lang,
102 }
103 }
104
105 pub fn indent_push(&mut self) {
107 self.indent += 1;
108 }
109
110 pub fn indent_pop(&mut self) {
112 assert!(self.indent > 0, "Already at top level indentation");
113 self.indent -= 1;
114 }
115
116 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 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 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 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 pub fn empty_line(&mut self) {
155 self.lines.push("\n".to_string());
156 }
157
158 pub fn multi_line(&mut self, s: &str) {
160 parse_multiline(s).into_iter().for_each(|l| self.line(&l));
161 }
162
163 pub fn comment(&mut self, s: impl AsRef<str>) {
165 self.line(format!("{} {}", self.lang.comment_token(), s.as_ref()));
168 }
169
170 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 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 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 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 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 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
252fn _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
262fn parse_multiline(s: &str) -> Vec<String> {
266 let expanded_tab = format!("{:-1$}", " ", SHIFTWIDTH);
268 let lines: Vec<String> = s.lines().map(|l| l.replace('\t', &expanded_tab)).collect();
269
270 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 let mut lines_iter = lines.iter().skip_while(|l| l.is_empty());
280 let mut trimmed = Vec::with_capacity(lines.len());
281
282 if let Some(s) = lines_iter.next().map(|l| l.trim()).map(|l| l.to_string()) {
284 trimmed.push(s);
285 }
286
287 let mut other_lines = if let Some(indent) = indent {
289 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 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
317pub struct Match {
326 expr: String,
327 arms: BTreeMap<(Vec<String>, String), BTreeSet<String>>,
328 catch_all: Option<String>,
330}
331
332impl Match {
333 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 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 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 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); 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}