1use std::cmp;
7use std::collections::{BTreeMap, BTreeSet};
8use std::fmt::Display;
9use std::fs;
10use std::io::Write;
11
12pub mod error;
13
14#[macro_export]
16macro_rules! loc {
17 () => {
18 $crate::FileLocation::new(file!(), line!())
19 };
20}
21
22pub struct FileLocation {
24 file: &'static str,
25 line: u32,
26}
27
28impl FileLocation {
29 pub fn new(file: &'static str, line: u32) -> Self {
30 Self { file, line }
31 }
32}
33
34impl core::fmt::Display for FileLocation {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{}:{}", self.file, self.line)
37 }
38}
39
40#[macro_export]
43macro_rules! fmtln {
44 ($fmt:ident, $fmtstring:expr, $($fmtargs:expr),*) => {
45 $fmt.line_with_location(format_args!($fmtstring, $($fmtargs),*), $crate::loc!())
46 };
47
48 ($fmt:ident, $arg:expr) => {
49 $fmt.line_with_location(format_args!($arg), $crate::loc!())
50 };
51
52 ($_:tt, $($args:expr),+) => {
53 compile_error!("This macro requires at least two arguments: the Formatter instance and a format string.")
54 };
55
56 ($_:tt) => {
57 compile_error!("This macro requires at least two arguments: the Formatter instance and a format string.")
58 };
59}
60
61#[derive(Debug, Clone, Copy)]
63pub enum Language {
64 Rust,
65 Isle,
66}
67
68impl Language {
69 pub fn should_append_location(&self, line: &str) -> bool {
71 match self {
72 Language::Rust => !line.ends_with(['{', '}']),
73 Language::Isle => true,
74 }
75 }
76
77 pub fn comment_token(&self) -> &'static str {
79 match self {
80 Language::Rust => "//",
81 Language::Isle => ";;",
82 }
83 }
84}
85
86static SHIFTWIDTH: usize = 4;
87
88fn spaces_for_indent(indent: usize) -> &'static str {
90 static INDENT_STR: &'static str = " ";
92 &INDENT_STR[..INDENT_STR.len().min(indent * SHIFTWIDTH)]
93}
94
95pub struct Formatter {
97 indent: usize,
98 lines: String,
99 lang: Language,
100}
101
102impl Formatter {
103 pub fn new(lang: Language) -> Self {
106 Self {
107 indent: 0,
108 lines: String::new(),
109 lang,
110 }
111 }
112
113 pub fn indent_push(&mut self) {
115 self.indent += 1;
116 }
117
118 pub fn indent_pop(&mut self) {
120 assert!(self.indent > 0, "Already at top level indentation");
121 self.indent -= 1;
122 }
123
124 pub fn indent<T, F: FnOnce(&mut Formatter) -> T>(&mut self, f: F) -> T {
126 self.indent_push();
127 let ret = f(self);
128 self.indent_pop();
129 ret
130 }
131
132 fn get_indent(&self) -> &'static str {
134 spaces_for_indent(self.indent)
135 }
136
137 pub fn line(&mut self, contents: impl Display) {
139 use std::fmt::Write;
140
141 let indent = self.get_indent();
142 write!(&mut self.lines, "{indent}{contents}\n").unwrap();
143 }
144
145 pub fn line_with_location(&mut self, contents: impl Display, location: FileLocation) {
148 use std::fmt::Write;
149
150 let indent = self.get_indent();
151
152 write!(&mut self.lines, "{indent}{contents}").unwrap();
153 if self.lang.should_append_location(&self.lines) {
155 let comment_token = self.lang.comment_token();
156 write!(&mut self.lines, " {comment_token} {location}").unwrap();
157 }
158 self.lines.push('\n');
159 }
160
161 pub fn empty_line(&mut self) {
163 self.lines.push('\n');
164 }
165
166 pub fn multi_line(&mut self, s: &str) {
168 parse_multiline(s).into_iter().for_each(|l| self.line(&l));
169 }
170
171 pub fn comment(&mut self, comment: impl Display) {
173 let comment_token = self.lang.comment_token();
176 self.line(format_args!("{comment_token} {comment}"));
177 }
178
179 pub fn doc_comment(&mut self, contents: impl AsRef<str>) {
181 assert!(matches!(self.lang, Language::Rust));
182 parse_multiline(contents.as_ref())
183 .iter()
184 .map(|l| {
185 if l.is_empty() {
186 "///".into()
187 } else {
188 format!("/// {l}")
189 }
190 })
191 .for_each(|s| self.line(s.as_str()));
192 }
193
194 pub fn add_block<T, F: FnOnce(&mut Formatter) -> T>(&mut self, start: &str, f: F) -> T {
197 assert!(matches!(self.lang, Language::Rust));
198 self.line(format_args!("{start} {{"));
199 let ret = self.indent(f);
200 self.line("}");
201 ret
202 }
203
204 pub fn add_match(&mut self, m: Match) {
206 assert!(matches!(self.lang, Language::Rust));
207 fmtln!(self, "match {} {{", m.expr);
208 self.indent(|fmt| {
209 for (&(ref fields, ref body), ref names) in m.arms.iter() {
210 let conditions = names
212 .iter()
213 .map(|name| {
214 if !fields.is_empty() {
215 format!("{} {{ {} }}", name, fields.join(", "))
216 } else {
217 name.clone()
218 }
219 })
220 .collect::<Vec<_>>()
221 .join(" |\n")
222 + " => {";
223
224 fmt.multi_line(&conditions);
225 fmt.indent(|fmt| {
226 fmt.line(body);
227 });
228 fmt.line("}");
229 }
230
231 if let Some(body) = m.catch_all {
233 fmt.line("_ => {");
234 fmt.indent(|fmt| {
235 fmt.line(body);
236 });
237 fmt.line("}");
238 }
239 });
240 self.line("}");
241 }
242
243 pub fn write(
245 &self,
246 filename: impl AsRef<std::path::Path>,
247 directory: &std::path::Path,
248 ) -> Result<(), error::Error> {
249 let path = directory.join(&filename);
250 eprintln!("Writing generated file: {}", path.display());
251 let mut f = fs::File::create(path)?;
252
253 f.write_all(self.lines.as_bytes())?;
254 Ok(())
255 }
256}
257
258fn parse_multiline(s: &str) -> Vec<String> {
262 let expanded_tab = spaces_for_indent(1);
264 let lines: Vec<String> = s.lines().map(|l| l.replace('\t', &expanded_tab)).collect();
265
266 let indent = lines
268 .iter()
269 .skip(1)
270 .filter(|l| !l.trim().is_empty())
271 .map(|l| l.len() - l.trim_start().len())
272 .min();
273
274 let mut lines_iter = lines.iter().skip_while(|l| l.is_empty());
276 let mut trimmed = Vec::with_capacity(lines.len());
277
278 if let Some(s) = lines_iter.next().map(|l| l.trim()).map(|l| l.to_string()) {
280 trimmed.push(s);
281 }
282
283 let mut other_lines = if let Some(indent) = indent {
285 lines_iter
287 .map(|l| &l[cmp::min(indent, l.len())..])
288 .map(|l| l.trim_end())
289 .map(|l| l.to_string())
290 .collect::<Vec<_>>()
291 } else {
292 lines_iter
293 .map(|l| l.trim_end())
294 .map(|l| l.to_string())
295 .collect::<Vec<_>>()
296 };
297
298 trimmed.append(&mut other_lines);
299
300 while let Some(s) = trimmed.pop() {
302 if s.is_empty() {
303 continue;
304 } else {
305 trimmed.push(s);
306 break;
307 }
308 }
309
310 trimmed
311}
312
313pub struct Match {
322 expr: String,
323 arms: BTreeMap<(Vec<String>, String), BTreeSet<String>>,
324 catch_all: Option<String>,
326}
327
328impl Match {
329 pub fn new(expr: impl Into<String>) -> Self {
331 Self {
332 expr: expr.into(),
333 arms: BTreeMap::new(),
334 catch_all: None,
335 }
336 }
337
338 fn set_catch_all(&mut self, clause: String) {
339 assert!(self.catch_all.is_none());
340 self.catch_all = Some(clause);
341 }
342
343 pub fn arm<T: Into<String>, S: Into<String>>(&mut self, name: T, fields: Vec<S>, body: T) {
345 let name = name.into();
346 assert!(
347 name != "_",
348 "catch all clause can't extract fields, use arm_no_fields instead."
349 );
350
351 let body = body.into();
352 let fields = fields.into_iter().map(|x| x.into()).collect();
353 let match_arm = self
354 .arms
355 .entry((fields, body))
356 .or_insert_with(BTreeSet::new);
357 match_arm.insert(name);
358 }
359
360 pub fn arm_no_fields(&mut self, name: impl Into<String>, body: impl Into<String>) {
362 let body = body.into();
363
364 let name = name.into();
365 if name == "_" {
366 self.set_catch_all(body);
367 return;
368 }
369
370 let match_arm = self
371 .arms
372 .entry((Vec::new(), body))
373 .or_insert_with(BTreeSet::new);
374 match_arm.insert(name);
375 }
376}
377
378#[cfg(test)]
379mod srcgen_tests {
380 use super::Formatter;
381 use super::Language;
382 use super::Match;
383 use super::parse_multiline;
384
385 #[test]
386 fn adding_arms_works() {
387 let mut m = Match::new("x");
388 m.arm("Orange", vec!["a", "b"], "some body");
389 m.arm("Yellow", vec!["a", "b"], "some body");
390 m.arm("Green", vec!["a", "b"], "different body");
391 m.arm("Blue", vec!["x", "y"], "some body");
392 assert_eq!(m.arms.len(), 3);
393
394 let mut fmt = Formatter::new(Language::Rust);
395 fmt.add_match(m);
396
397 let expected_lines = r#"match x {
398 Green { a, b } => {
399 different body
400 }
401 Orange { a, b } |
402 Yellow { a, b } => {
403 some body
404 }
405 Blue { x, y } => {
406 some body
407 }
408}
409"#;
410 assert_eq!(fmt.lines, expected_lines);
411 }
412
413 #[test]
414 fn match_with_catchall_order() {
415 let mut m = Match::new("x");
417 m.arm("Orange", vec!["a", "b"], "some body");
418 m.arm("Green", vec!["a", "b"], "different body");
419 m.arm_no_fields("_", "unreachable!()");
420 assert_eq!(m.arms.len(), 2); let mut fmt = Formatter::new(Language::Rust);
423 fmt.add_match(m);
424
425 let expected_lines = r#"match x {
426 Green { a, b } => {
427 different body
428 }
429 Orange { a, b } => {
430 some body
431 }
432 _ => {
433 unreachable!()
434 }
435}
436"#;
437 assert_eq!(fmt.lines, expected_lines);
438 }
439
440 #[test]
441 fn parse_multiline_works() {
442 let input = "\n hello\n world\n";
443 let expected = vec!["hello", "world"];
444 let output = parse_multiline(input);
445 assert_eq!(output, expected);
446 }
447
448 #[test]
449 fn formatter_basic_example_works() {
450 let mut fmt = Formatter::new(Language::Rust);
451 fmt.line("Hello line 1");
452 fmt.indent_push();
453 fmt.comment("Nested comment");
454 fmt.indent_pop();
455 fmt.line("Back home again");
456 let expected_lines = vec![
457 "Hello line 1\n",
458 " // Nested comment\n",
459 "Back home again\n",
460 ];
461 assert_eq!(fmt.lines, expected_lines.join(""));
462 }
463
464 #[test]
465 fn get_indent_works() {
466 let mut fmt = Formatter::new(Language::Rust);
467 let expected_results = vec!["", " ", " ", ""];
468
469 let actual_results = Vec::with_capacity(4);
470 (0..3).for_each(|_| {
471 fmt.get_indent();
472 fmt.indent_push();
473 });
474 (0..3).for_each(|_| fmt.indent_pop());
475 fmt.get_indent();
476
477 actual_results
478 .into_iter()
479 .zip(expected_results)
480 .for_each(|(actual, expected): (String, &str)| assert_eq!(&actual, expected));
481 }
482
483 #[test]
484 fn fmt_can_add_type_to_lines() {
485 let mut fmt = Formatter::new(Language::Rust);
486 fmt.line(format!("pub const {}: Type = Type({:#x});", "example", 0));
487 let expected_lines = "pub const example: Type = Type(0x0);\n";
488 assert_eq!(fmt.lines, expected_lines);
489 }
490
491 #[test]
492 fn fmt_can_add_indented_line() {
493 let mut fmt = Formatter::new(Language::Rust);
494 fmt.line("hello");
495 fmt.indent_push();
496 fmt.line("world");
497 let expected_lines = vec!["hello\n", " world\n"];
498 assert_eq!(fmt.lines, expected_lines.join(""));
499 }
500
501 #[test]
502 fn fmt_can_add_doc_comments() {
503 let mut fmt = Formatter::new(Language::Rust);
504 fmt.doc_comment("documentation\nis\ngood");
505 let expected_lines = vec!["/// documentation\n", "/// is\n", "/// good\n"];
506 assert_eq!(fmt.lines, expected_lines.join(""));
507 }
508
509 #[test]
510 fn fmt_can_add_doc_comments_with_empty_lines() {
511 let mut fmt = Formatter::new(Language::Rust);
512 fmt.doc_comment(
513 r#"documentation
514 can be really good.
515
516 If you stick to writing it.
517"#,
518 );
519 let expected_lines = r#"/// documentation
520/// can be really good.
521///
522/// If you stick to writing it.
523"#;
524 assert_eq!(fmt.lines, expected_lines);
525 }
526}