Как разобрать одиночные токены в макросах ржавчины

Я начинаю играть с макросами Rust и пришел, чтобы попробовать этот маленький практический пример. Я хочу определить макрос, который расширяется до инициализации переменной (имя не имеет значения) типа i32 (например, но не очень важно) и ряд операций с этой переменной, в данном случае var += 1 или var -= 1 и наконец это позвонит println!("{}", var), Макрос будет принимать серию токенов на основе + а также - это соответствует операциям, описанным выше.

Так, например:

operate_integer![+++---]

будет расширяться до:

let mut var: i32 = 0;
var += 1;
var += 1;
var += 1;
var -= 1;
var -= 1;
var -= 1;
print!("{}", var);

Я решил использовать 2 макроса для этого, один для упаковки инициализации и печати, а другой для оценки +- лексемы:

Основой будет:

macro_rules! operate_integer {
    // $($all_tokens:tt)* should match everything, it will be forward to the helper macro
    ($($all_tokens:tt)*) => {
        let mut var : i32 = 0;
        operate_integer_helper![$($all_tokens:tt)*]
        print!("{}", var);
    }
}

Помощник расширит операции:

macro_rules! operate_integer_helper {
    // the idea is that it matches a `+` followed by more tokens
    (+$($t:tt)*) => {
        val += 1;
        operate_integer_helper![$($t:tt)*] // we recursively handle the remaining tokens
    }

    (-$($t:tt)*) => {
        val -= 1;
        operate_integer_helper![$($t:tt)*]
    }
}

Это, конечно, не работает, происходит сбой компиляции со следующей ошибкой ( Playground):

error: no rules expected the token `(`
   --> src/lib.rs:102:5
    |
102 |     (+$($t:tt)*) => {
    |     ^ no rules expected this token in macro call

Я немного застрял. Я знаю, что, возможно, мне не хватает многих концепций, так как я только начал, и я был бы очень признателен за помощь в понимании работы с макросами. Заранее спасибо!

1 ответ

Решение

Вы на самом деле очень близко! Осталась только пара мелких ошибок. (Если вы хотите узнать больше о макросах, читайте только один пункт за раз и попробуйте продвинуться оттуда самостоятельно!)

  • При использовании (повторяющихся) метапеременных вы больше не указываете тип мета-переменной. Так что это $($t:tt)* в шаблоне макроса, но если вы хотите использовать его, это $($t)*!

  • Если у вас есть несколько правил в определении макроса, вам нужно завершить каждое правило точкой с запятой.

    macro_rules! {
        (+ $(t:tt)*) => { ... };
        (- $(t:tt)*) => { ... };
    }
    
  • Компилятору Rust всегда нужно знать, хотите ли вы расширить свой макрос в выражение или оператор (ы). Так как вы генерируете список операторов, а не одно выражение, вы должны добавить точку с запятой к вызову ваших макросов! Это означает, что в main() но также все вызовы макросов вспомогательных макросов внутри определения макроса.

  • Поскольку вызов макроса да создает новый синтаксический контекст и все идентификаторы (имена) доступны только в их синтаксическом контексте, вспомогательный макрос не может использовать var (даже после исправления опечатки val -> var). Поэтому вместо этого вы должны передать это имя вспомогательному макросу:

    macro_rules! operate_integer {
        ($($all_tokens:tt)*) => {
            let mut var: i32 = 0;
            operate_integer_helper![var $($all_tokens)*];  // <-- pass identifier 
            println!("{}", var);
        }
    }
    
    macro_rules! operate_integer_helper {
        ($var:ident +$($t:tt)*) => {              // <- accept identifier
            $var += 1;                            // <- use identifier
            operate_integer_helper![$var $($t)*]
        };
    
        ($var:ident -$($t:tt)*) => {
            $var -= 1;
            operate_integer_helper![$var $($t)*]
        };
    }
    
  • Сделав все это, вы получите ошибку "неожиданный конец вызова макроса". Это потому что у вас нет правила остановки рекурсии! Таким образом, вы должны добавить новое правило в ваш вспомогательный макрос: ($var:ident) => {};, Это правило используется, когда есть только имя и нет + или же - жетоны остались.

А теперь: это работает!

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

С этим, это результирующий код.

Другие вопросы по тегам