Решение с macro_rules!
макросом
Реализовать это с помощью декларативных макросов (macro_rules!
) немного сложно, но возможно.Однако необходимо использовать несколько хитростей.
Но сначала, вот код ( Детская площадка ):
macro_rules! replace {
// This is the "public interface". The only thing we do here is to delegate
// to the actual implementation. The implementation is more complicated to
// call, because it has an "out" parameter which accumulates the token we
// will generate.
($x:ident, $y:ident, $($e:tt)*) => {
replace!(@impl $x, $y, [], $($e)*)
};
// Recursion stop: if there are no tokens to check anymore, we just emit
// what we accumulated in the out parameter so far.
(@impl $x:ident, $y:ident, [$($out:tt)*], ) => {
$($out)*
};
// This is the arm that's used when the first token in the stream is an
// identifier. We potentially replace the identifier and push it to the
// out tokens.
(@impl $x:ident, $y:ident, [$($out:tt)*], $head:ident $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* replace!(@replace $x $y $head)],
$($tail)*
)
}};
// These arms are here to recurse into "groups" (tokens inside of a
// (), [] or {} pair)
(@impl $x:ident, $y:ident, [$($out:tt)*], ( $($head:tt)* ) $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* ( replace!($x, $y, $($head)*) ) ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], [ $($head:tt)* ] $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* [ replace!($x, $y, $($head)*) ] ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], { $($head:tt)* } $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* { replace!($x, $y, $($head)*) } ],
$($tail)*
)
}};
// This is the standard recusion case: we have a non-identifier token as
// head, so we just put it into the out parameter.
(@impl $x:ident, $y:ident, [$($out:tt)*], $head:tt $($tail:tt)*) => {{
replace!(@impl $x, $y, [$($out)* $head], $($tail)*)
}};
// Helper to replace the identifier if its the needle.
(@replace $needle:ident $replacement:ident $i:ident) => {{
// This is a trick to check two identifiers for equality. Note that
// the patterns in this macro don't contain any meta variables (the
// out meta variables $needle and $i are interpolated).
macro_rules! __inner_helper {
// Identifiers equal, emit $replacement
($needle $needle) => { $replacement };
// Identifiers not equal, emit original
($needle $i) => { $i };
}
__inner_helper!($needle $i)
}}
}
fn main() {
let foo = 3;
let bar = 7;
let z = 5;
dbg!(replace!(abc, foo, bar * 100 + z)); // no replacement
dbg!(replace!(bar, foo, bar * 100 + z)); // replace `bar` with `foo`
}
Он выводит:
[src/main.rs:56] replace!(abc , foo , bar * 100 + z) = 705
[src/main.rs:57] replace!(bar , foo , bar * 100 + z) = 305
Как эторабота?
Есть два основных трюка, которые нужно понять, прежде чем разбираться в этом макросе: накопление с понижением и как проверить два идентификатора на равенство .
Более того, просто чтобы быть уверенным: @foobar
вещи в начале шаблона макроса не являются специальной функцией, а просто соглашением пометить внутренние вспомогательные макросы (см. Также: "МаленькийКнига Макросов ", StackOverflow вопрос ).
Сброс накопления хорошо описан в этой главе" Маленькая книга "макроса ржавчины ".Важная часть:
Все макросы в Rust должны приводить к полному, поддерживаемому элементу синтаксиса (например, выражению, элементу и т. Д.).Это означает, что невозможно развернуть макрос в частичную конструкцию.
Но часто необходимо получить частичные результаты, например, при работе с токеном для токена с некоторыми входными данными.Чтобы решить эту проблему, в основном есть параметр «out», который представляет собой просто список токенов, который увеличивается с каждым рекурсивным вызовом макроса.Это работает, потому что входные данные макросов могут быть произвольными токенами и не обязательно должны быть допустимой конструкцией Rust.
Этот шаблон имеет смысл только для макросов, которые работают как «инкрементные анализаторы TT», что и делает мое решение.Также есть глава об этом шаблоне в TLBORM .
Вторым ключевым моментом является проверка двух идентификаторов на равенство .Это сделано с интересной уловкой: макрос определяет новый макрос, который затем немедленно используется.Давайте посмотрим на код:
(@replace $needle:ident $replacement:ident $i:ident) => {{
macro_rules! __inner_helper {
($needle $needle) => { $replacement };
($needle $i) => { $i };
}
__inner_helper!($needle $i)
}}
Давайте рассмотрим два разных вызова:
replace!(@replace foo bar baz)
: это расширится до:
macro_rules! __inner_helper {
(foo foo) => { bar };
(foo baz) => { baz };
}
__inner_helper!(foo baz)
И вызов inner_helper!
теперь явно принимает второй шаблон, в результате чего baz
.
replace!(@replace foo bar foo)
с другой стороны расширяется до:
macro_rules! __inner_helper {
(foo foo) => { bar };
(foo foo) => { foo };
}
__inner_helper!(foo foo)
На этот раз вызов inner_helper!
берет первый шаблон, в результате чего bar
.
Я узнал этот трюк из ящика, который предлагает в основном только то, что: макроспроверка двух идентификаторов на равенство.Но, к сожалению, я больше не могу найти этот ящик.Дайте мне знать, если вы знаете имя этого ящика!
Однако эта реализация имеет несколько ограничений:
Как инкрементный ТТ-мункер, он повторяетсяза каждый токен на входе.Таким образом, легко достичь предела рекурсии (который может быть увеличен, но он не оптимален).Можно было бы написать нерекурсивную версию этого макроса, но пока я не нашел способа сделать это.
macro_rules!
макросы немного странные, когдаэто касается идентификаторов.Представленное выше решение может вести себя странно с self
в качестве идентификатора.См. эту главу для получения дополнительной информации по этой теме.
Решение с proc-macro
Конечноэто также можно сделать с помощью proc-макроса.Это также включает в себя менее странные трюки.Мое решение выглядит так:
extern crate proc_macro;
use proc_macro::{
Ident, TokenStream, TokenTree,
token_stream,
};
#[proc_macro]
pub fn replace(input: TokenStream) -> TokenStream {
let mut it = input.into_iter();
// Get first parameters
let needle = get_ident(&mut it);
let _comma = it.next().unwrap();
let replacement = get_ident(&mut it);
let _comma = it.next().unwrap();
// Return the remaining tokens, but replace identifiers.
it.map(|tt| {
match tt {
// Comparing `Ident`s can only be done via string comparison right
// now. Note that this ignores syntax contexts which can be a
// problem in some situation.
TokenTree::Ident(ref i) if i.to_string() == needle.to_string() => {
TokenTree::Ident(replacement.clone())
}
// All other tokens are just forwarded
other => other,
}
}).collect()
}
/// Extract an identifier from the iterator.
fn get_ident(it: &mut token_stream::IntoIter) -> Ident {
match it.next() {
Some(TokenTree::Ident(i)) => i,
_ => panic!("oh noes!"),
}
}
Использование этого макроса proc с примером main()
сверху работает точно так же.
Примечание : обработка ошибок здесь была проигнорирована, чтобы пример был коротким.Пожалуйста, прочитайте этот вопрос о том, как создавать отчеты об ошибках в макросах proc.
Кроме этого, я думаю, что этот код не нуждается в таком большом количестве объяснений.Эта версия макроса proc также не страдает от проблемы предела рекурсии, как макрос macro_rules!
.