Я пишу макрос для удобного сопоставления вложенной структуры в типизированной переменной 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)
}
}
}
}
}
Наконец, вот еще одна ссылка на игровую площадку , которая показывает отдельные шаги рекурсивного расширения. Это очень плотно.