enum
型付き変数のネストされた構造をコンパイル時のテンプレートに簡単に一致させるマクロを作成しています。Rust のパターン マッチングを利用して、構造の特定の場所に特定の値を強制するか、変数を他の興味深い場所にバインドするという考え方です。私の実装では基本的な考え方は機能しますが、ネストされたパターンでは機能しません。問題は、マクロ入力の一部が解析される$<name>:pat
と、後で として解析できないことだと思います$<name>:tt
。
パターンという用語のあいまいな使用を避けるために、Rust のドキュメントに従って次の表記法を使用します。
- パターンは
match
アームやステートメントに現れるものでif let
あり、フラグメント指定子によってマクロで照合されます$<name>:pat
。 - マッチャーは、マクロ内の構文規則の左側です。
- テンプレートは、マクロの展開方法を決定するマクロへの入力の一部です。
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
れ、マクロの 2 番目の引数として渡されるブロック内の変数として使用できるようになります。
これは、上記のマクロを作成するための私の努力です:
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")}
};
}
ネストされていないテンプレートでは期待どおりに機能し、ネストされたテンプレートではマクロ呼び出しを手動でネストできます。ただし、1 回のマクロ呼び出しでネストされたテンプレートを直接指定すると、コンパイル エラーで失敗します。
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`
[Str(name), _, _]
エラーは、型の不一致を引き起こす 3 番目のマクロ ルールによって受け入れられる単一のスライス パターンとして一致すると言っていると思われます。ただし、2 番目のルールで一連のパターンに分解できるように、トークン ツリーにしたいと考えています。
2 番目のルールを に変更しようとしました($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)
}
}
}
}
}
最後に、再帰的展開の個々のステップを示す別のプレイグラウンド リンクを次に示します。とても濃いです。