quote!() { /* proc-macro */ }
Expand description
Whitespace sensitive quasi-quoting.
This and the quote_in! macro is the thing that this library revolves around.
It provides a flexible and intuitive mechanism for efficiently generating beautiful code directly inside of Rust.
Note that this macro can only detect line changes if it’s built under a
nightly
compiler. See the main genco documentation for more information.
use genco::prelude::*;
let hash_map = &dart::import("dart:collection", "HashMap");
let tokens: dart::Tokens = quote! {
print_greeting(String name) {
print($[str](Hello $(name)));
}
$hash_map<int, String> map() {
return new $hash_map<int, String>();
}
};
println!("{}", tokens.to_file_string()?);
§Interpolation
Variables are interpolated using $
, so to include the variable test
, you
would write $test
. Interpolated variables must implement FormatInto.
Expressions can be interpolated with $(<expr>)
.
Note: The
$
punctuation itself can be escaped by repeating it twice. So$$
would produce a single$
token.
use genco::prelude::*;
let hash_map = rust::import("std::collections", "HashMap");
let tokens: rust::Tokens = quote! {
struct Quoted {
field: $hash_map<u32, u32>,
}
};
assert_eq!(
vec![
"use std::collections::HashMap;",
"",
"struct Quoted {",
" field: HashMap<u32, u32>,",
"}",
],
tokens.to_file_vec()?,
);
The following is an expression interpolated with $(<expr>)
.
use genco::prelude::*;
let tokens: genco::Tokens = quote! {
hello $("world".to_uppercase())
};
assert_eq!("hello WORLD", tokens.to_string()?);
Interpolations are evaluated in the same scope as the macro, so you can
freely make use of Rust operations like the try keyword (?
) if
appropriate:
use std::error::Error;
use genco::prelude::*;
fn age_fn(age: &str) -> Result<rust::Tokens, Box<dyn Error>> {
Ok(quote! {
fn age() {
println!("You are {} years old!", $(str::parse::<u32>(age)?));
}
})
}
§Escape Sequences
Because this macro is whitespace sensitive, it might sometimes be necessary to provide hints of where whitespace should be inserted.
quote!
trims any trailing and leading whitespace that it sees. So
quote!(Hello )
is the same as quote!(Hello)
. To include a space at the
end, we can use the special $[' ']
escape sequence:
quote!(Hello$[' '])
.
The available escape sequences are:
-
$[' ']
— Inserts spacing between tokens. This corresponds to the Tokens::space function. -
$['\r']
— Inserts a push operation. Push operations makes sure that any following tokens are on their own dedicated line. This corresponds to the Tokens::push function. -
$['\n']
— Inserts a forced line. Line operations makes sure that any following tokens have an empty line separating them. This corresponds to the Tokens::line function.
use genco::prelude::*;
let numbers = 3..=5;
let tokens: Tokens<()> = quote!(foo$['\r']bar$['\n']baz$[' ']biz);
assert_eq!("foo\nbar\n\nbaz biz", tokens.to_string()?);
§String Quoting
Literal strings like "hello"
are automatically quoted for the target
language according to its Lang::write_quoted implementation.
use genco::prelude::*;
let tokens: java::Tokens = quote! {
"hello world 😊"
$(quoted("hello world 😊"))
$("\"hello world 😊\"")
$[str](hello world $[const]("😊"))
};
assert_eq!(
vec![
"\"hello world \\ud83d\\ude0a\"",
"\"hello world \\ud83d\\ude0a\"",
"\"hello world 😊\"",
"\"hello world \\ud83d\\ude0a\"",
],
tokens.to_file_vec()?,
);
§Efficient String Quoting
It’s worth investigating the different forms of tokens produced by the above example.
- The first one is a static quoted string.
- The second one is a boxed quoted string, who’s content will be copied and is stored on the heap.
- The third one is a static literal which bypasses language quoting entirely.
- Finally the fourth one is an interpolated string. They are really neat,
and will be covered more in the next section. It’s worth noting that
$("😊")
is used, because 😊 is not a valid identifier in Rust. So this example showcases how strings can be directly embedded in an interpolation.
Here you can see the items produced by the macro.
use genco::tokens::{Item, ItemStr};
assert_eq!(
vec![
Item::OpenQuote(false),
Item::Literal(ItemStr::Static("hello world 😊")),
Item::CloseQuote,
Item::Push,
Item::OpenQuote(false),
Item::Literal(ItemStr::Box("hello world 😊".into())),
Item::CloseQuote,
Item::Push,
Item::Literal(ItemStr::Static("\"hello world 😊\"")),
Item::Push,
Item::OpenQuote(false),
Item::Literal(ItemStr::Static("hello world 😊")),
Item::CloseQuote
],
tokens,
);
§Quoted String Interpolation
Some languages support interpolating values into strings.
Examples of these are:
- JavaScript - With template literals
`Hello ${a}`
(note the backticks). - Dart - With interpolated strings like
"Hello $a"
or"Hello ${a + b}"
.
The quote! macro supports this through $[str](<content>)
. This will
produce literal strings with the appropriate language-specific quoting and
string interpolation formats used.
Components of the string are runtime evaluated with the typical variable
escape sequences $ident
, $(<expr>)
. In order to interpolate the string
at compile time we can instead make use of $[const](<content>)
like you can see with the smile below:
use genco::prelude::*;
let smile = "😊";
let t: js::Tokens = quote!($[str](Hello $[const](smile) $world));
assert_eq!("`Hello 😊 ${world}`", t.to_string()?);
Interpolated values are specified with $(<quoted>)
. And $
itself is
escaped by repeating it twice through $$
. The <quoted>
section is
interpreted the same as in the quote! macro, but is whitespace sensitive.
This means that $(foo)
is not the same as $(foo )
since the latter will
have a space preserved at the end.
use genco::prelude::*;
let smile = "😊";
let t: dart::Tokens = quote!($[str](Hello $[const](smile) $(world)));
assert_eq!("\"Hello 😊 $world\"", t.to_string()?);
let t: dart::Tokens = quote!($[str](Hello $[const](smile) $(a + b)));
assert_eq!("\"Hello 😊 ${a + b}\"", t.to_string()?);
let t: js::Tokens = quote!($[str](Hello $[const](smile) $(world)));
assert_eq!("`Hello 😊 ${world}`", t.to_string()?);
§Control Flow
quote! provides some limited mechanisms for control flow inside of the macro for convenience. The supported mechanisms are:
- Loops -
$(for <bindings> in <expr> [join (<quoted>)] => <quoted>)
. - Conditionals -
$(if <pattern> => <quoted>)
. - Match Statements -
$(match <expr> { [<pattern> => <quoted>,]* })
.
§Loops
To repeat a pattern you can use $(for <bindings> in <expr> { <quoted> })
,
where <expr>
is an iterator.
It is also possible to use the more compact $(for <bindings> in <expr> => <quoted>)
(note the arrow).
<quoted>
will be treated as a quoted expression, so anything which works
during regular quoting will work here as well, with the addition that
anything defined in <bindings>
will be made available to the statement.
use genco::prelude::*;
let numbers = 3..=5;
let tokens: Tokens<()> = quote! {
Your numbers are: $(for n in numbers => $n$[' '])
};
assert_eq!("Your numbers are: 3 4 5", tokens.to_string()?);
§Joining Loops
You can add join (<quoted>)
to the end of a repetition.
The expression specified in join (<quoted>)
is added between each
element produced by the loop.
Note: The argument to
join
is whitespace sensitive, so leading and trailing is preserved.join (,)
andjoin (, )
would therefore produce different results.
use genco::prelude::*;
let numbers = 3..=5;
let tokens: Tokens<()> = quote! {
Your numbers are: $(for n in numbers join (, ) => $n).
};
assert_eq!("Your numbers are: 3, 4, 5.", tokens.to_string()?);
§Conditionals
You can specify a conditional with $(if <pattern> => <then>)
where
<pattern>
is an pattern or expression evaluating to a bool
, and <then>
is a quoted expressions.
It’s also possible to specify a condition with an else branch, by using
$(if <pattern> { <then> } else { <else> })
. <else>
is also a quoted
expression.
use genco::prelude::*;
fn greeting(hello: bool, name: &str) -> Tokens<()> {
quote!(Custom Greeting: $(if hello {
Hello $name
} else {
Goodbye $name
}))
}
let tokens = greeting(true, "John");
assert_eq!("Custom Greeting: Hello John", tokens.to_string()?);
let tokens = greeting(false, "John");
assert_eq!("Custom Greeting: Goodbye John", tokens.to_string()?);
The <else>
branch is optional, conditionals which do not have an else
branch and evaluated to false
won’t produce any tokens:
use genco::prelude::*;
fn greeting(hello: bool, name: &str) -> Tokens<()> {
quote!(Custom Greeting:$(if hello {
$[' ']Hello $name
}))
}
let tokens = greeting(true, "John");
assert_eq!("Custom Greeting: Hello John", tokens.to_string()?);
let tokens = greeting(false, "John");
assert_eq!("Custom Greeting:", tokens.to_string()?);
§Match Statements
You can specify a match expression using $(match <expr> { [<pattern> => <quoted>,]* }
, where <expr>
is an evaluated expression that is match
against each subsequent <pattern>
. If a pattern matches, the arm with the
matching <quoted>
block is evaluated.
use genco::prelude::*;
fn greeting(name: &str) -> Tokens<()> {
quote!(Hello $(match name {
"John" | "Jane" => $("Random Stranger"),
other => $other,
}))
}
let tokens = greeting("John");
assert_eq!("Hello Random Stranger", tokens.to_string()?);
let tokens = greeting("Mio");
assert_eq!("Hello Mio", tokens.to_string()?);
If a match arm contains parenthesis (=> (<quoted>)
), the expansion will be
whitespace sensitive. Allowing leading and trailing whitespace to be
preserved:
use genco::prelude::*;
fn greeting(name: &str) -> Tokens<()> {
quote!(Hello$(match name {
"John" | "Jane" => ( $("Random Stranger")),
other => ( $other),
}))
}
let tokens = greeting("John");
assert_eq!("Hello Random Stranger", tokens.to_string()?);
let tokens = greeting("Mio");
assert_eq!("Hello Mio", tokens.to_string()?);
The following is an example with more complex matching:
use genco::prelude::*;
enum Greeting {
Named(&'static str),
Unknown,
}
fn greeting(name: Greeting) -> Tokens<()> {
quote!(Hello $(match name {
Greeting::Named("John") | Greeting::Named("Jane") => $("Random Stranger"),
Greeting::Named(other) => $other,
Greeting::Unknown => $("Unknown Person"),
}))
}
let tokens = greeting(Greeting::Named("John"));
assert_eq!("Hello Random Stranger", tokens.to_string()?);
let tokens = greeting(Greeting::Unknown);
assert_eq!("Hello Unknown Person", tokens.to_string()?);
let tokens = greeting(Greeting::Named("Mio"));
assert_eq!("Hello Mio", tokens.to_string()?);
§Variable assignment
You can use $(let <binding> = <expr>)
to define variables with their value.
This is useful within loops to compute values from iterator items.
use genco::prelude::*;
let names = ["A.B", "C.D"];
let tokens: Tokens<()> = quote! {
$(for name in names =>
$(let (first, second) = name.split_once('.').unwrap())
$first and $second.
)
};
assert_eq!("A and B.\nC and D.", tokens.to_string()?);
Variables can also be mutable:
use genco::prelude::*;
let path = "A.B.C.D";
let tokens: Tokens<()> = quote! {
$(let mut items = path.split('.'))
$(if let Some(first) = items.next() =>
First is $first
)
$(if let Some(second) = items.next() =>
Second is $second
)
};
assert_eq!("First is A\nSecond is B", tokens.to_string()?);
§Scopes
You can use $(ref <binding> { <expr> })
to gain access to the current
token stream. This is an alternative to existing control flow operators if
you want to run some custom code during evaluation which is otherwise not
supported. This is called a scope.
For a more compact variant you can omit the braces with $(ref <binding> => <expr>)
.
use genco::prelude::*;
fn quote_greeting(surname: &str, lastname: Option<&str>) -> rust::Tokens {
quote! {
Hello $surname$(ref toks {
if let Some(lastname) = lastname {
toks.space();
toks.append(lastname);
}
})
}
}
assert_eq!("Hello John", quote_greeting("John", None).to_string()?);
assert_eq!("Hello John Doe", quote_greeting("John", Some("Doe")).to_string()?);
§Whitespace Detection
The quote! macro has the following rules for dealing with indentation and spacing.
Spaces — Two tokens that are separated are spaced. Regardless of how
many spaces there are between them. This can be controlled manually by
inserting the $[' ']
escape sequence in the token stream.
use genco::prelude::*;
let tokens: rust::Tokens = quote! {
fn test() {
println!("Hello... ");
println!("World!");
}
};
assert_eq!(
vec![
"fn test() {",
" println!(\"Hello... \");",
"",
" println!(\"World!\");",
"}",
],
tokens.to_file_vec()?,
);
Line breaking — Line breaks are detected by leaving two empty lines
between two tokens. This can be controlled manually by inserting the
$['\n']
escape in the token stream.
use genco::prelude::*;
let tokens: rust::Tokens = quote! {
fn test() {
println!("Hello... ");
println!("World!");
}
};
assert_eq!(
vec![
"fn test() {",
" println!(\"Hello... \");",
"",
" println!(\"World!\");",
"}",
],
tokens.to_file_vec()?,
);
Indentation — Indentation is determined on a row-by-row basis. If a column is further in than the one on the preceeding row, it is indented one level deeper.
If a column starts shallower than a preceeding, non-whitespace only row, it will be matched against previously known indentation levels. Failure to match a previously known level is an error.
All indentations inserted during the macro will be unrolled at the end of it. So any trailing indentations will be matched by unindentations.
use genco::prelude::*;
let tokens: rust::Tokens = quote! {
fn test() {
println!("Hello... ");
println!("World!");
}
};
assert_eq!(
vec![
"fn test() {",
" println!(\"Hello... \");",
"",
" println!(\"World!\");",
"}",
],
tokens.to_file_vec()?,
);
Example showcasing an indentation mismatch:
use genco::prelude::*;
let tokens: rust::Tokens = quote! {
fn test() {
println!("Hello... ");
println!("World!");
}
};
---- src\lib.rs - (line 150) stdout ----
error: expected 4 less spaces of indentation
--> src\lib.rs:157:9
|
10 | println!("World!");
| ^^^^^^^