こちらの記事は Rustマクロ冬期講習アドベントカレンダー 12日目の記事です!
今回も小ネタ紹介です。以下、今回の出典元です。
Metavariables and Expansion Redux - The Little Book of Rust Macros
今回は小ネタ【闇編】ということで、別に闇でもなんでもないのですが、「そんな制約が...」と少しガッカリしてしまうものを持ってきました。
ガッカリ1: 宣言マクロのマッチは上のアームに吸われる!!!
macro_rules! checker {
($i:item) => { println!("アイテム"); };
($($t:tt)*) => { println!("その他"); };
}
fn main() {
// ttにマッチさせるつもり
checker!(hoge);
}
error: expected one of `!` or `::`, found `<eof>`
--> src/main.rs:8:14
|
2 | ($i:item) => { println!("アイテム"); };
| ------- while parsing argument for this `item` macro fragment
...
8 | checker!(hoge);
| ^^^^ expected one of `!` or `::`
error: could not compile `playground` (bin "playground") due to 1 previous error
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=206ebd5fa957f30db37a716df040b994
マクロのアームは マッチが失敗したからといって別なマッチを試してくれるとは限らない ということです。失敗するアームに入力が吸われると言いましょうか。ともかく「えぇ...」な仕様です。
match
式のアームのようにもう少し直感的に書ければ良いのですけどね...
ちなみに次のように順序を変えればコンパイルは通りますが2つ目の item
アームに意味がなくなります
macro_rules! checker {
($($t:tt)*) => { println!("その他"); };
($i:item) => { println!("アイテム"); };
}
fn main() {
checker!(hoge); // => その他
// itemにマッチさせるつもり
checker!(fn fuga() {}); // => その他
}
うーん...ケチ!Little Bookには「より具体的に記述されたパターンを上に持っていくべき」みたいなこと書いていますが、もし希望通りのマッチ結果にならない時はともかく色々な順番を試した方が良さそうですし、試したところで希望通りのマッチにならないことが結構あるわけです。
ガッカリ2: メタ変数の後ろに制約がある場合がある
どこかの回でマッチアームには「めっちゃヘンテコなパターンも書ける」と書いたのですが、嘘でしたという話です
macro_rules! strange_macro {
(@~ $name:ident ~%) => {
println!("Hey, {}!", stringify!($name));
};
}
fn main() {
strange_macro!(@~ hoge ~%);
}
macro_rules! strange_macro {
(@~ $e:expr ~%) => { // OKの場合とはフラグメント指定子が異なるのみ
println!("Hey, {}!", stringify!($e));
};
}
fn main() {
strange_macro!(@~ 10 ~%);
}
error: `$e:expr` is followed by `~`, which is not allowed for `expr` fragments
--> src/main.rs:2:17
|
2 | (@~ $e:expr ~%) => { // OKの場合とはフラグメント指定子が異なるのみ
| ^ not allowed after `expr` fragments
|
= note: allowed there are: `=>`, `,` or `;`
error: could not compile `playground` (bin "playground") due to 1 previous error
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8edf80c158376ca2502a7372851c7e71
コンパイルエラーに「expr
の後続は =>
か ,
か ;
のみが許されています」と記載されているように、特定のフラグメント指定子について後続に許されるトークンに縛りがあります!
フラグメント指定子 | 許容されるトークン |
---|---|
stmt または expr
|
=> , , , ;
|
pat |
=> , , , = , if , in , (| ) (※1) |
pat_param |
=> , , , = , if , in , |
|
path または ty
|
=> , , , = , | , ; , : , > , >> , [ , { , as , where , ブロック(block )要素 |
vis |
, , priv (※2) 以外の識別子, 型(ty )を始められるトークン(< など), 「ident , ty , path 」のうちのどれかのメタ変数 |
その他 | とくに制限なし |
※1: |
なのですがMarkdownが崩れてしまうので|
で代替しています。なお、 pat
で |
が許されるのは最新エディション(2021)以降です。互換性維持のために pat_param
というものがあります。
※2: priv
は現在は使われていない予約語です。
(スペースではなく無)が可視性 vis
としてマッチする都合上、可視性に関わってそうな priv
がマッチすると都合が悪そうです。
Little Bookには後続制限は「マクロ入力解釈を変更する将来の構文変更に備えて〜」みたいなことが書いていますが、確かに任意のフラグメント指定子について後続をなんでも許可すると、そもそもマッチングが複雑になり読みにくくなってしまいます。
macro_rules! compare {
($a:expr < $b:expr) => {};
}
fn main() {
compare!(10 < 20);
}
10 < 20
はこれはこれで式です。 10
まで読めば良いのか <
も読まなければならないのか不明瞭なので、これがコンパイルエラーになってもあまり文句はありません。もし無理やり式として解釈できたとして、将来式として解釈される構文が増えたりした時に同じように働く保証はありません。そのためにエラーになる方に倒しているのだと思います。
もし後続トークンが原因で怒られたら素直に別な表現に変えましょう。
ガッカリ3: 先読みなど高度なマッチングはしてくれない
高度な正規表現みたいなことはしてくれないらしく次のような書き方はできません。
macro_rules! ambiguity {
($($i:ident)* $i2:ident) => { };
}
fn main() {
ambiguity!(an_identifier);
}
error: local ambiguity when calling macro `ambiguity`: multiple parsing options: built-in NTs ident ('i') or ident ('i2').
--> src/main.rs:6:16
|
6 | ambiguity!(an_identifier);
| ^^^^^^^^^^^^^
error: could not compile `playground` (bin "playground") due to 1 previous error
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=60f806218646c30824382e9809c004c8
マクロ終わりの )
が来ていることを先読みできれば i2
のみにマッチして i
は0個だったと判定してくれますが、「曖昧だ 💢」と怒られてしまっています。
ここまでよりわかることとして、定義するパターンはこっちが忖度して決める必要がありそうです。
ガッカリ4: 一度マッチしてしまうとそのフラグメント指定子で固定されてしまう
Little Bookにて "One aspect of substitution that often surprises people is that substitution is not token-based, despite very much looking like it." 、「置換はトークンベースに見えるが実際はそうではない」と記述されている部分です。
複数のマクロがネストしている時に問題になります。Little Bookにある例です。
macro_rules! capture_then_match_tokens {
($e:expr) => {match_tokens!($e)};
}
macro_rules! match_tokens {
($a:tt + $b:tt) => {"got an addition"};
(($i:ident)) => {"got an identifier"};
($($other:tt)*) => {"got something else"};
}
fn main() {
println!("{}\n{}\n{}\n",
match_tokens!((caravan)),
match_tokens!(3 + 6),
match_tokens!(5));
println!("{}\n{}\n{}",
capture_then_match_tokens!((caravan)),
capture_then_match_tokens!(3 + 6),
capture_then_match_tokens!(5));
}
got an identifier
got an addition
got something else
got something else
got something else
got something else
capture_then_match_tokens
マクロでキャプチャされてしまうと、その中身がなんであれ全て expr
の塊であったと見做されてしまい、改めて分解してのマッチングができなくなってしまっています!
By parsing the input into an AST node, the substituted result becomes un-destructible; i.e. you cannot examine the contents or match against it ever again.
入力を AST ノードに解析すると、置換された結果は破壊不可能になります。 つまり、その内容を調べたり、再度一致させたりすることはできません。
tt
ident
lifetime
でのマッチングだとフラグメント指定子固定がないので、別なマクロで改めて分解したいものはこちらでマッチングすると良いらしいです。宣言マクロどんな闇仕様してるんだよ...
まとめ・所感
「宣言マクロ罠多すぎ...」って気持ちになる小ネタ集を紹介しました!コンパイルがうまくいかない時はパターンをゴニョゴニョするしかなさそうですね。
どうしてもマクロで実現したい一方で宣言マクロに限界を感じたら、さっさと手続きマクロに移行してしまうのもありだと思います。