Как заменить один идентификатор в выражении другим с помощью макроса Rust? - PullRequest
10 голосов
/ 02 мая 2019

Я пытаюсь создать макрос, который выполняет некоторое преобразование кода и должен иметь возможность анализировать свой собственный синтаксис.Вот самый простой пример, который я могу себе представить:

replace!(x, y, x * 100 + z) ~> y * 100 + z

Этот макрос должен иметь возможность заменить первый идентификатор на второй в выражении, представленном в качестве третьего параметра.Макрос должен иметь некоторое представление о языке третьего параметра (который в моем конкретном случае, в отличие от примера, не будет анализироваться в Rust) и рекурсивно применяться к нему.

Какой самый эффективный способпостроить такой макрос в Rust?Я знаю о proc_macro подходе и macro_rules!.Однако я не уверен, является ли macro_rules! достаточно мощным, чтобы справиться с этим, и я не смог найти много документации о том, как создавать свои собственные преобразования, используя proc_macro.Кто-нибудь может указать мне правильное направление?

1 Ответ

11 голосов
/ 02 мая 2019

Решение с 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!.

...