blob: b6992dc15fd08a9d45d72cebe612b036475e927c [file] [log] [blame]
Marcel Hlopko3164eee2021-08-24 20:09:22 +00001// Part of the Crubit project, under the Apache License v2.0 with LLVM
2// Exceptions. See /LICENSE for license information.
3// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
Michael Forster523dbd42021-10-12 11:05:44 +00005use anyhow::bail;
Marcel Hlopko3164eee2021-08-24 20:09:22 +00006use anyhow::Result;
7use proc_macro2::TokenStream;
8use proc_macro2::TokenTree;
9
10use std::fmt::Write;
11
Googler42d540f2021-09-29 06:37:23 +000012fn is_ident_or_literal(tt: &TokenTree) -> bool {
13 matches!(tt, TokenTree::Ident(_) | TokenTree::Literal(_))
14}
15
Marcel Hlopko3164eee2021-08-24 20:09:22 +000016/// Produces C++ source code out of the token stream.
17///
18/// Notable features:
Michael Forsterbee84482021-10-13 08:35:38 +000019/// * quote! cannot produce a single `#` token (that is not immediately followed
20/// by `(`, `[`, `{`, or variable interpolation). For cases when we need `#`
21/// to be produced in the C++ source code use the placeholder
22/// `__HASH_TOKEN__`.
23/// * The Rust tokenizer ignores newlines as they are not significant for Rust.
24/// For C++ they are (for example there needs to be a newline after `#include
25/// "foo/bar.h"`). We are also using explict newlines for making the generated
26/// Rust/C++ source code more readable. Use the placeholder `__NEWLINE__` to
27/// insert a newline character.
28/// * `TokenStream` cannot encode comments, so we use the placeholder
29/// `__COMMENT__`, followed by a string literal.
Michael Forsterdb8101a2021-10-08 06:56:03 +000030pub fn tokens_to_string(tokens: TokenStream) -> Result<String> {
Michael Forster0de2b8c2021-10-11 08:28:49 +000031 let mut result = String::new();
32 let mut it = tokens.into_iter().peekable();
33 while let Some(tt) = it.next() {
Marcel Hlopko3164eee2021-08-24 20:09:22 +000034 match tt {
35 TokenTree::Ident(ref tt) if tt == "__NEWLINE__" => writeln!(result)?,
36 TokenTree::Ident(ref tt) if tt == "__HASH_TOKEN__" => write!(result, "#")?,
37
Michael Forster523dbd42021-10-12 11:05:44 +000038 TokenTree::Ident(ref tt) if tt == "__COMMENT__" => {
39 if let Some(TokenTree::Literal(lit)) = it.next() {
40 writeln!(
41 result,
42 "// {}",
43 lit.to_string().trim_matches('"').replace("\\n", "\n// ")
44 )?;
45 } else {
46 bail!("__COMMENT__ must be followed by a literal")
47 }
48 }
49
Michael Forster0de2b8c2021-10-11 08:28:49 +000050 _ => {
51 write!(result, "{}", tt)?;
Googler42d540f2021-09-29 06:37:23 +000052
Michael Forster0de2b8c2021-10-11 08:28:49 +000053 // Insert spaces between tokens only when they are needed to separate
54 // identifiers or literals from each other.
55 if is_ident_or_literal(&tt)
56 && matches!(it.peek(), Some(tt_next) if is_ident_or_literal(tt_next))
57 {
58 write!(result, " ")?;
59 }
60 }
61 }
Marcel Hlopko3164eee2021-08-24 20:09:22 +000062 }
63 Ok(result)
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 use super::Result;
71 use quote::quote;
72
73 #[test]
74 fn test_simple_token_stream() -> Result<()> {
75 let token_stream = quote! {
76 struct Foo {}
77
78 impl Bar for Foo {
79 fn bar(&self) {}
80 }
81 };
Googler42d540f2021-09-29 06:37:23 +000082 assert_eq!(
Michael Forsterdb8101a2021-10-08 06:56:03 +000083 tokens_to_string(token_stream.clone())?,
Googler42d540f2021-09-29 06:37:23 +000084 "struct Foo{ }impl Bar for Foo{ fn bar (& self) { } }"
85 );
86 Ok(())
87 }
88
89 #[test]
90 fn test_space_idents_and_literals() -> Result<()> {
91 let token_stream = quote! { foo 42 bar 23 };
Michael Forsterdb8101a2021-10-08 06:56:03 +000092 assert_eq!(tokens_to_string(token_stream.clone())?, "foo 42 bar 23");
Googler42d540f2021-09-29 06:37:23 +000093 Ok(())
94 }
95
96 #[test]
97 fn test_dont_space_punctuation() -> Result<()> {
98 let token_stream = quote! { foo+42+bar+23 };
Michael Forsterdb8101a2021-10-08 06:56:03 +000099 assert_eq!(tokens_to_string(token_stream.clone())?, "foo+42+bar+23");
Marcel Hlopko3164eee2021-08-24 20:09:22 +0000100 Ok(())
101 }
102
103 #[test]
104 fn test_newline_token() -> Result<()> {
105 let token_stream = quote! { a __NEWLINE__ b };
Michael Forster0de2b8c2021-10-11 08:28:49 +0000106 assert_eq!(tokens_to_string(token_stream.clone())?, "a \nb");
Marcel Hlopko3164eee2021-08-24 20:09:22 +0000107 Ok(())
108 }
109
110 #[test]
111 fn test_hash_token() -> Result<()> {
112 let token_stream = quote! { a __HASH_TOKEN__ b };
Michael Forster0de2b8c2021-10-11 08:28:49 +0000113 assert_eq!(tokens_to_string(token_stream.clone())?, "a #b");
Marcel Hlopko3164eee2021-08-24 20:09:22 +0000114 Ok(())
115 }
Googler42d540f2021-09-29 06:37:23 +0000116
117 #[test]
118 fn test_include_standard_header() -> Result<()> {
119 let token_stream = quote! { __HASH_TOKEN__ include <cstddef> };
Michael Forster0de2b8c2021-10-11 08:28:49 +0000120 assert_eq!(tokens_to_string(token_stream.clone())?, "#include<cstddef>");
Googler42d540f2021-09-29 06:37:23 +0000121 Ok(())
122 }
Michael Forster523dbd42021-10-12 11:05:44 +0000123
124 #[test]
125 fn test_comments() -> Result<()> {
126 let token_stream = quote! { __COMMENT__ "line1\nline2" };
127 assert_eq!(tokens_to_string(token_stream.clone())?, "// line1\n// line2\n");
128 Ok(())
129 }
130
131 #[test]
132 fn test_invalid_comment() -> Result<()> {
133 assert!(tokens_to_string(quote! { __COMMENT__ }).is_err());
134 assert!(tokens_to_string(quote! { __COMMENT__ ident }).is_err());
135 Ok(())
136 }
Marcel Hlopko3164eee2021-08-24 20:09:22 +0000137}