Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/cranelift/srcgen/src/lib.rs
1692 views
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
548