Как расширить подшаблоны в рекурсивных макросах? - PullRequest
1 голос
/ 21 июня 2019

Я пишу макрос для удобного сопоставления вложенной структуры в типизированной переменной enum с шаблоном времени компиляции. Идея состоит в том, чтобы использовать сопоставление с образцом в Rust для обеспечения определенных значений в определенных местах структуры или привязки переменных к другим интересным местам. Основная идея работает в моей реализации, но она не подходит для вложенных шаблонов. Я полагаю, что проблема заключается в том, что после того, как часть входных данных макроса была проанализирована как $<name>:pat, она не может быть позже проанализирована как $<name>:tt.

Чтобы избежать неоднозначного использования термина pattern , я буду использовать следующие обозначения в соответствии с документацией Rust:

  • A pattern - это то, что появляется в match руках, в if let операторах и сопоставляется в макросах спецификатором фрагмента $<name>:pat.
  • A matcher - левая часть правила синтаксиса в макросе.
  • A template - это часть ввода в мой макрос, которая определяет, как макрос будет расширяться.

Детская площадка MCVE

Это упрощенная версия типа enum, который я использую:

#[derive(Debug, Clone)]
enum TaggedValue {
    Str(&'static str),
    Seq(Vec<TaggedValue>),
}

Например, следующее выражение

use TaggedValue::*;
let expression = Seq(vec![
    Str("define"),
    Seq(vec![Str("mul"), Str("x"), Str("y")]),
    Seq(vec![Str("*"), Str("x"), Str("y")]),
]);

может соответствовать этому вызову макроса:

match_template!(
    &expression,                               // dynamic input structure
    { println!("fn {}: {:?}", name, body) },   // action to take after successful match
    [Str("define"), [Str(name), _, _], body]   // template to match against
);

Здесь при успешном совпадении идентификаторы name и body связаны с соответствующими подэлементами в expression и становятся доступными в качестве переменных в блоке, передаваемом в качестве второго аргумента макросу.

Это моя попытка написать макрос:

macro_rules! match_template {
    // match sequence template with one element
    ($exp:expr, $action:block, [$single:pat]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], $action, $single)
        } else {
            panic!("mismatch")
        }
    };

    // match sequence template with more than one element
    ($exp:expr, $action:block, [$first:pat, $($rest:tt)*]) => {
        if let Seq(seq) = $exp {
            // match first pattern in sequence against first element of $expr
            match_template!(&seq[0], {
                // then match remaining patterns against remaining elements of $expr
                match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
            }, $first)
        } else {
            panic!("mismatch")
        }
    };

    // match a non sequence template and perform $action on success
    ($exp:expr, $action:block, $atom:pat) => {
        if let $atom = $exp $action else {panic!("mismatch")}
    };
}

Он работает как положено для не вложенных шаблонов, а для вложенных шаблонов я могу вручную вкладывать вызовы макросов. Однако непосредственное указание вложенного шаблона в одном вызове макроса завершается неудачно с ошибкой компиляции.

match_template!(
    &expression,
    {
        match_template!(
            signature,
            { println!("fn {}: {:?}", name, body) },
            [Str(name), _, _]
        )
    },
    [Str("define"), signature, body]
);
// prints:
//   fn mul: Seq([Str("*"), Str("x"), Str("y")])

match_template!(
    &expression,
    { println!("fn {}: {:?}", name, body) },
    [Str("define"), [Str(name), _, _], body]
);
// error[E0529]: expected an array or slice, found `TaggedValue`
//   --> src/main.rs:66:25
//    |
// 66 |         [Str("define"), [Str(name), _, _], body]
//    |                         ^^^^^^^^^^^^^^^^^ pattern cannot match with input type `TaggedValue`

Детская площадка MCVE

Я подозреваю, что ошибка говорит о том, что [Str(name), _, _] сопоставляется как шаблон с одним срезом, который принимается третьим правилом макроса, где это вызывает несоответствие типов. Однако я хочу, чтобы это было дерево токенов, чтобы второе правило могло разложить его на последовательность шаблонов.

Я пытался изменить второе правило на ($exp:expr, $action:block, [$first:tt, $($rest:tt)*]) =>, но это только вызывает ошибку на внешнем уровне.

Какие изменения необходимы для макроса, чтобы он мог рекурсивно развернуть такие шаблоны?

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

Я ожидаю, что это вызов макроса расширится (игнорируя несоответствия ветвей для краткости. Кроме того, я смоделировал макро-гигиену путем постфикса переменной seq):

// macro invocation
match_template!(
    &expression,
    { println!("fn {}: {:?}", name, body) },
    [Str("define"), [Str(name), _, _], body]
);

// expansion
if let Seq(seq_1) = &expression {
    if let Str("define") = &seq_1[0] {
        if let Seq(seq_1a) = Seq(seq_1[1..].into()) {
            if let Seq(seq_2) = &seq_1a[0] {
                if let Str(name) = &seq_2[0] {
                    if let Seq(seq_2a) = Seq(seq_2[1..].into()) {
                        if let _ = &seq_2a[0] {
                            if let Seq(seq_2b) = Seq(seq_2a[1..].into()) {
                                if let _ = &seq_2b[0] {
                                    if let Seq(seq_1b) = Seq(seq_1a[1..].into()) {
                                        if let body = &seq_1b[0] {
                                            { println!("fn {}: {:?}", name, body) }
                                        }
                                    }
                                }
                            }
                        } 
                    } 
                } 
            } 
        } 
    } 
} 

Полное расширение немного многословно, но эта слегка укороченная версия отражает суть того, что должно произойти:

if let Seq(seq) = &expression {
    if let Str("define") = &seq[0] {
        if let Seq(signature) = &seq[1] {
            if let Str(name) = &signature[0] {
                if let body = &seq[2] {
                    println!("fn {}: {:?}", name, body)
                }
            }
        }
    }
}

Наконец, вот еще одна ссылка на игровую площадку , которая показывает отдельные шаги рекурсивного расширения. Это очень плотно.

1 Ответ

1 голос
/ 24 июня 2019

Действительно, похоже, проблема в том, что макрос соответствует разделенному запятыми списку шаблонов. Таким образом, во входных данных [Str("define"), [Str(name), _, _], body] макрос интерпретирует внутренний [...] как шаблон среза, который не может соответствовать выражению типа TaggedValue.

Решение состоит в том, чтобы расширить входные данные как деревья токенов. Однако для этого требуется небольшой трюк, поскольку одно дерево токенов не может представлять каждый шаблон. В частности, шаблон вида Variant(value) состоит из двух токенов: Variant и (value). Эти два токена могут быть объединены обратно в шаблон перед вызовом терминального (не повторяющегося) правила макроса.

Например, правило для сопоставления такого шаблона в одноэлементном шаблоне начинается следующим образом:

($exp:expr, $action:block, [$single_variant:tt $single_value:tt]) =>

Эти токены передаются вместе на другой вызов макроса с

match_template!(&seq[0], $action, $single_variant $single_value)

где они соответствуют одному шаблону правилом терминала

($exp:expr, $action:block, $atom:pat) =>

Окончательное определение макроса содержит два дополнительных правила для учета Variant(value) шаблонов:

macro_rules! match_template {
    ($exp:expr, $action:block, [$single:tt]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], $action, $single)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, [$single_variant:tt $single_value:tt]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], $action, $single_variant $single_value)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, [$first:tt, $($rest:tt)*]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], {
                match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
            }, $first)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, [$first_variant:tt $first_value:tt, $($rest:tt)*]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], {
                match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
            }, $first_variant $first_value)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, $atom:pat) => {
        if let $atom = $exp $action else {panic!("mismatch")}
    };
}

Вот ссылка на полный пример: детская площадка .

...