Rustマクロの薄い本
訳注: 本書はThe Little Book of Rust Macros(Lukas Wirth氏による改訂版)の非公式日本語訳です🦀🇯🇵
Note: 翻訳元はDaniel Keep氏による原著(2016年の中頃より更新停止)を引き継いだもので、mdBookを利用するように改変してあります。
本書のHTML版はこちらから読めます。リポジトリはこちらにあります。
中国語訳はこちら。
本書の目標は、Rustのマクロ(宣言的マクロ(Macros by Example
)と手続き的マクロ(WIP)の両方)に関するRustコミュニティの集合知を蒸留することです。
ですから、(プルリクエストという形での)内容の追加も(issueという形の)リクエストも大歓迎です。
もし不明な点があれば、気軽に質問してください。よくわからない記述があれば、わかりやすくしてほしい旨を恐れずissueにぶつけてください。
本書をできる限り最高の学習資料にするのが目標です。
この本の原著は、私がRust言語を学んでいた頃に宣言的マクロ (Macros by Example)スタイルのマクロについて理解する大いなる助けになりました。 残念なことに、原著は2016年4月を最後に更新が途絶えてしまいました。その一方で、Rust言語自体もマクロシステムも進化し続けています。 そんなわけで私は、原著をアップデートし、最新に保ち、さらには新たな発見を書き加えるという仕事を引き受けたのです。 Rustに入門する方々がマクロシステムという手こずりがちなRust言語の一機能について理解を深めようとするにあたり、本書が助けになれば幸いです。
本書は読者がRustの基本的な知識を持っていることを仮定して書かれています。マクロに関係ない言語機能や構成要素については説明しません。 マクロに関する事前知識は不要です。 The Book1 の7章までを読んで理解しておくのが必須事項です。とはいえ、大部分を読んでおくことをおすすめします。
訳注: 日本語版はこちら
謝辞
原著の著者であるDaniel Keep氏、および原著へのコントリビューターの皆様(こちらで確認できる)に深く感謝申し上げます。
ライセンス
本書は原著のライセンスを継承します。よって、本書の使用はCreative Commons Attribution-ShareAlike 4.0 International LicenseとMIT licenseの両方の下で許諾されます2。
訳注: 日本語版についても同様の扱いとします。詳しくは本プロジェクトのリポジトリのREADMEをご覧ください。
構文拡張 (Syntax Extensions)
Rustのさまざまなマクロシステムの話に入る前に、それらの基礎となるより全般的な機構: 構文拡張 (syntax extensions) について考察するのは有意義でしょう。
そのためには、まずどのようにしてRustのソースコードがコンパイラ、ひいてはユーザ定義のマクロや手続き的マクロの基礎となる機構によって処理されるのかについて考えていかねばなりません。
Note: 本書では以後、構文拡張 (syntax extension) という用語をすべての種類のRustマクロに通ずる一般論を語る際に用います。これは、今後導入されるであろう、
macro
キーワードを用いる宣言的マクロ2.0の提案に伴って生じうる混乱を軽減するためです1。
訳注: 言いたいことがよく分からないが、おそらく「Rustのマクロ全般を指して "macro" と呼ぶことにすると、(宣言的マクロ2.0で導入される予定の)macro
キーワードを用いたマクロのみを指すように見えてしまう。それを避けるために、Rustのマクロ全般を指す用語として "syntax extension" という言葉を導入するよ」ということだと思われる。
ソースコード解析
トークン化
Rustプログラムのコンパイルの最初のステップはトークン化です。 これはソースコードをトークン(これ以上分割できない字句単位。プログラミング言語の「単語」)の列に変換するステップです。 Rustにはさまざまな種類のトークンがあります。例えば:
- 識別子:
foo
,Bambous
,self
,we_can_dance
,LaCaravane
, … - リテラル:
42
,72u32
,0_______0
,1.0e-40
,"ferris was here"
, … - 予約語:
_
,fn
,self
,match
,yield
,macro
, … - 記号:
[
,:
,::
,?
,~
,@
1, …
…など。上記において注意すべき点がいくつかあります。まず、self
は識別子であり、かつ予約語でもあるということです。
ほとんどすべての状況でself
は予約語ですが、識別子として扱われることも確かにあるのです。これについては後述します(たくさんの呪詛とともに)。
次に、予約語のリストはyield
やmacro
といった怪しげな要素を含んでいます。これらは実際言語の一部ではないのですが、コンパイラは予約語として認識します—将来の利用のために予約されているのです。
<-
は「名残」です。文法からは削除されたものの、字句解析器には残っているのです。
最後に、::
が区別されたトークンであることに注意してください。:
というトークンが2つ連続したものとは異なります。複数文字からなるすべての記号トークンについて同じことがいえます(Rust 1.2以降)。2
@
には用途があるのですが、ほとんどの人は完全に忘れてしまっているようです: @
はパターンの中で使い、パターンの非終端部分を名前に束縛します (訳注: 「パターンの非終端部分(non-terminal part of the pattern)」は、@束縛においてマッチ条件を表すパターンの部分を指すものと思われる)。
技術的には、Rustは現時点(1.46)で2つの字句解析器を持ちます。単一文字の記号のみをトークンとして出力するrustc_lexer
と、複数文字の記号を個別のトークンとみなすrustc_parse
内のlexerの2つです(訳注: 翻訳時点の最新版(1.67)においても同様)。
比較点として、まさにこの段階でマクロの展開を行う言語が存在する一方で、Rustはそうではありません。 例えば、C/C++のマクロは事実上この時点で処理されます。3
次のコードが動作するのはこのためです。4
#define SUB void
#define BEGIN {
#define END }
SUB main() BEGIN
printf("Oh, the horror!\n");
END
実際のところ、CのプリプロセッサはC言語自体とは異なる字句構造を用いていますが、その違いはほとんど重要ではありません。
これが動作すべきか否かというのは全く別の問題です。
構文解析
次の段階は構文解析で、これはトークンの列を抽象構文木(Abstract Syntax Tree, AST)に変換します。
これはプログラムの構文構造をメモリ上に構築する処理を伴います。例えば、トークン列1 + 2
は次のようなものに変換されます:
┌─────────┐ ┌─────────┐
│ BinOp │ ┌╴│ LitInt │
│ op: Add │ │ │ val: 1 │
│ lhs: ◌ │╶┘ └─────────┘
│ rhs: ◌ │╶┐ ┌─────────┐
└─────────┘ └╴│ LitInt │
│ val: 2 │
└─────────┘
抽象構文木はプログラム全体の構造を含みますが、それは字句上の情報のみに基づくものです。
例えば、特定の式がa
と呼ばれる変数を参照しているのをコンパイラが知っているとしても、a
が何なのか、ひいてはそれがどこからやってきたのかを知る術はありません。
抽象構文木が構築された後、ついにマクロが処理されます。ですが、マクロの処理の考察に入る前に、トークン木について考えておかねばなりません。
トークン木 (Token trees)
トークン木は、トークンと抽象構文木の中間に位置するものです。 第一に、ほとんどすべてのトークンはトークン木でもあります。より正確にいえば、トークンは葉にあたります。 もう一種類トークン木の葉になりうるものが存在しますが、それについては後述します。
基本的なトークンのうち唯一葉ではないのが、(...)
, [...]
, {...}
といった「グループ化」トークンです。
これらはトークン木の内部ノードにあたり、トークン木に構造をもたらすものです。具体的な例を挙げると、次のトークン列は…
a + b + (c + d[0]) + e
次のようなトークン木にパースされます:
«a» «+» «b» «+» «( )» «+» «e»
╭────────┴──────────╮
«c» «+» «d» «[ ]»
╭─┴─╮
«0»
これは、この式が生成する抽象構文木とは何の関連もないということに注意してください。 1つの根ノードがあるのではなく、根のレベルに7つのトークン木があるのです。 参考までに、この式の抽象構文木を載せておきます:
┌─────────┐
│ BinOp │
│ op: Add │
┌╴│ lhs: ◌ │
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ BinOp │
│ name: a │ │ op: Add │
└─────────┘ ┌╴│ lhs: ◌ │
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ BinOp │
│ name: b │ │ op: Add │
└─────────┘ ┌╴│ lhs: ◌ │
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ BinOp │╶┘ └─────────┘ └╴│ Var │
│ op: Add │ │ name: e │
┌╴│ lhs: ◌ │ └─────────┘
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ Index │
│ name: c │ ┌╴│ arr: ◌ │
└─────────┘ ┌─────────┐ │ │ ind: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ LitInt │
│ name: d │ │ val: 0 │
└─────────┘ └─────────┘
抽象構文木とトークン木の違いはよく理解しておいてください。マクロを書く際は、これらを別々のものとして、両方とも扱う必要があるのです。
もう一つ注意すべき点は、トークン木が組になっていない括弧、あるいはネスト構造がおかしいグループを含むことはないということです。
ASTにおけるマクロ
先述した通り、Rustにおけるマクロの処理は抽象構文木が構築された後に行われます。 したがって、マクロを呼び出すのに使う構文は言語の構文上正しいものでなければなりません。 実際、いくつかの「構文拡張(syntax extension)」形式がRustの構文に組み込まれています。 具体的には、以下の4つの形式があります:
# [ $arg ]
例:#[derive(Clone)]
,#[no_mangle]
, …# ! [ $arg ]
例:#![allow(dead_code)]
,#![crate_name="blang"]
, …$name ! $arg
例:println!("Hi!")
,concat!("a", "b")
, …name ! $arg0 $arg1
例:macro_rules! dummy { () => {}; }
最初の2つは属性で、アイテム、式、文にアノテーションをつけるものです。 属性は組み込み属性、proc-macro属性、derive属性に分類できます。 proc-macro属性とderive属性はRustが提供する第二のマクロシステムである手続き的マクロによって実装できます。 一方、組み込み属性はコンパイラが実装している属性です。
3つめの形式 $name ! $arg
は関数形式マクロです。これはmacro_rules!
によるマクロ、macro
によるマクロ、そして手続き的マクロを呼び出すのに使えます。
macro_rules!
で定義されたマクロだけに限定されているわけではないことに注意してください: これは一般的な構文拡張の形式なのです。
例えば、format!
はmacro_rules!
によるマクロですが、(format!
を実装するのに用いられている)format_args!
はそうではなく、コンパイラ組み込みのマクロです。
4つめの形式は原則としてマクロに対して使える形式ではありません。
実際、この形式が使われるのはmacro_rules!
構文そのものに対してのみに限られます。
さて、3番めの形式について、Rustのパーサはいかにして考えうるすべての構文拡張に対して ($name ! $arg)
の$arg
の中身がどうなっているのかを知るのでしょうか?
知る必要がない、というのが答えです。
代わりに、構文拡張の呼び出しにおける引数部分は単一のトークン木になります。
より具体的にいえば、単一の葉でないトークン木、すなわち(...)
, [...]
または {...}
になります。
この知識があれば、パーサが以下の呼び出し形式をどのように理解するのかがはっきりわかるはずです:
bitflags! {
struct Color: u8 {
const RED = 0b0001,
const GREEN = 0b0010,
const BLUE = 0b0100,
const BRIGHT = 0b1000,
}
}
lazy_static! {
static ref FIB_100: u32 = {
fn fib(a: u32) -> u32 {
match a {
0 => 0,
1 => 1,
a => fib(a-1) + fib(a-2)
}
}
fib(100)
};
}
fn main() {
use Color::*;
let colors = vec![RED, GREEN, BLUE];
println!("Hello, World!");
}
上記の呼び出し形式はありとあらゆる種類のRustコードを含んでいるように見えるかもしれませんが、パーサが見ているのはただの無意味なトークン木の集まりにすぎません。 このことをよりはっきりさせるために、すべての構文上の「ブラックボックス」を⬚に置き換えてみると、次のようになります:
bitflags! ⬚
lazy_static! ⬚
fn main() {
let colors = vec! ⬚;
println! ⬚;
}
繰り返しになりますが、パーサは⬚に関して何の仮定も置きません。⬚に含まれるトークンを覚えておくだけで、それを理解しようとすることはありません。 すなわち⬚に入るのは何であっても、無効なRustコードであってもかまわないのです! どうしてこれがいいことなのかについては、あとで説明します。
さて、このことは形式1と2における$arg
、そして形式4における2つの引数にも当てはまるのでしょうか? 大体はあっています。
形式1と2の$arg
は少し違っていて、そのままトークン木になるわけではなく、後に=
とリテラル式かトークン木が続く単純パス(simple path) になります。
これについては、手続き的マクロについてのしかるべき章で深堀りしていきます。
ここで重要なのは、この形式においても入力を表現するのにトークン木を用いているということです。
4つめの形式は概してより特別で、非常に限定された構文(とはいえ、これもトークン木を用いる)のみを受け付けます。
この形式の詳細は現時点では重要ではないので、重要になるまで脇においておきましょう。
以上のことから得られる結論は次の通りです:
- Rustには複数の種類の構文拡張が存在する。
- ただ
$name! $arg
という形をした何かを見るだけでは、それがどの種類の構文拡張なのかを知ることはできない。それはmacro_rules!
によるマクロかもしれないし、手続き的マクロ、さらには組み込みのマクロでさえありうる。 - すべての
!
がつくマクロ呼び出し (形式3) の入力は単一の葉でないトークン木である。 - 構文拡張は抽象構文木の一部としてパースされる。
最後の点が最も重要で、それはこのことが重大な意味合いを含むためです。 構文拡張は抽象構文木にパースされるため、明示的にサポートされた場所にしか現れ得ないのです。 具体的には、構文拡張は次の場所に書くことができます:
- パターン
- 文
- 式
- アイテム (
impl
アイテムを含む) - 型
このリストに含まれないものの例:
- 識別子
- match式のアーム
- 構造体のフィールド
前者のリストに含まれない場所で構文拡張を使うことは、絶対に、どう足掻こうとも不可能です。
展開
マクロの展開(expansion)は比較的単純な作業です。 抽象構文木を構築し終えてからコンパイラがプログラムの意味を理解しようとし始めるまでの間のどこかで、コンパイラはすべての構文拡張を展開します。
これは、抽象構文木を走査し、構文拡張の呼び出し箇所を見つけ、展開形で置き換えるという処理を伴います。
コンパイラが構文拡張を実行する際、コンパイラは呼び出し結果がその文脈に合致する構文要素のいずれかとしてパースできることを期待します。 例えば、構文拡張をモジュールスコープで呼び出したならば、コンパイラは呼び出し結果をアイテムを表す抽象構文木のノードとしてパースすることになります。 構文拡張を式が来るべき位置で呼び出したならば、コンパイラは結果を式の抽象構文木ノードとしてパースします。
実のところ、コンパイラは構文拡張の呼び出し結果を以下のいずれかに変換できます:
- 式
- パターン
- 型
- 0個以上のアイテム
- 0個以上の文
言いかえれば、構文拡張をどこで呼び出したかによって、その結果がどう解釈されるかが決まるということです。
コンパイラは、構文拡張を展開した結果の抽象構文木ノードで構文拡張の呼び出しに対応するノードをそっくり置き換えます。 これは構造を考慮した操作であり、テキスト上の操作ではありません!
例えば、以下のコードを考えてみましょう:
let eight = 2 * four!();
これに対応する部分抽象構文木を図解すると次のようになります:
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────────┐
│ LitInt │╶┘ └─────────┘ └╴│ Macro │
│ val: 2 │ │ name: four │
└────────┘ │ body: () │
└────────────┘
文脈より、four!()
は式として展開されなければなりません(初期化子(initializer)1には式しか来ないため)。
よって、実際の展開形が何であれ、それは完全な式として解釈されることになります。
この場合、four!()
は1 + 3
のような式に展開されるものとして定義されていると仮定できます。
結果として、この呼び出しを展開すると抽象構文木は次のように変化します:
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ LitInt │╶┘ └─────────┘ └╴│ BinOp │
│ val: 2 │ │ op: Add │
└────────┘ ┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────┐
│ LitInt │╶┘ └─────────┘ └╴│ LitInt │
│ val: 1 │ │ val: 3 │
└────────┘ └────────┘
訳注: 初期化子(initializer)とは、変数の初期化文の右辺のこと。
これは次のように書き下すことができます:
let eight = 2 * (1 + 3);
展開形に含まれていないにもかかわらず、括弧が付け足されていることに注意してください。 コンパイラは常に構文拡張の展開形を完全な抽象構文木のノードとして扱うのであって、ただのトークンの列として扱うのではないことを思い出してください。 別の言い方をすれば、複雑な式を明示的に括弧で囲まなくても、コンパイラが展開の結果を「誤解」したり、評価の順序を入れ替えたりすることはないということです。
構文拡張の展開形が抽象構文木のノードとして扱われるということをよく理解しておきましょう。この設計はさらに2つの意味を持ちます:
- 構文拡張は、呼び出し位置の制約に加えて、その位置においてパーサが期待する種類の抽象構文木ノードにしか展開できないという制約を受ける。
- 上記の制約の帰結として、構文拡張は不完全な、あるいは構文的に不正な構造には決して展開されない。
構文拡張の展開について、さらにもう一つ注意すべきことがあります。ある構文拡張が別の構文拡張の呼び出しを含む何かに展開されたらどうなるのでしょうか。
例えば、four!
の別定義を考えてみましょう。それが1 + three!()
に展開されるとしたら、どうなるのでしょうか?
let x = four!();
これは次のように展開されます:
let x = 1 + three!();
これはコンパイラが追加の構文拡張呼び出しの展開結果を確認し、展開することで解決されます。 したがって、2段階めの展開ステップにより上記のコードは次のように変換されます:
let x = 1 + 3;
ここから得られる結論は、構文拡張の展開はすべての呼び出しが完全に展開されるのに必要なだけの「パス」にわたって行われるということです。
いや、これには語弊があります。 実際には、コンパイラは断念するまでに実行を試みる再帰的パスの数に上限を設けています。 これは構文拡張の再帰制限(recursion limit)として知られており、デフォルト値は128となっています。 もし128回めの展開が構文拡張呼び出しを含んでいたら、コンパイラは再帰制限を超過したことを示すエラーとともに実行を中断します。
この制限は#![recursion_limit="…"]
属性を用いて引き上げることができるものの、クレート単位でしか設定できません。
上限の引き上げはコンパイル時間に影響を与える可能性があるため、基本的にはできる限り構文拡張が再帰制限を超えないように努めることをおすすめします。
衛生性(Hygiene)
衛生性(Hygiene)はマクロに関する重要な概念です。 衛生性とは、マクロが周辺のコンテキストに影響を与えたり逆に影響受けたりすることなく、自身の構文コンテキストの中で動作する能力を指します。 言いかえれば、任意の構文拡張の呼び出しは周辺のコンテキストに干渉するべきではないということを意味します。
理想的には、Rustのすべての構文拡張は完全に衛生的であってほしいところですが、残念ながらそうではないので、十分衛生的ではない構文拡張を書くことがないよう注意を払うべきです。 ここから一般的な衛生性の概念の説明に入ります。なお、これについてはRustが提供する別の種類の構文拡張の衛生性に関する章でも言及します。
衛生性は主に構文拡張が出力する識別子とパス(path)に影響します。 簡単にいうと、構文拡張が生成した識別子を、構文拡張を呼び出した環境から参照できないとき、構文拡張はその識別子に関して衛生的であるといえます。 同様に、構文拡張の内部で利用されている識別子が、構文拡張の外部で定義されたものを参照できないときも、構文拡張は衛生的であるといえます。
Note: 「生成」「利用」という用語は、識別子がどの位置に出現しているかを表すものです。
struct Foo {}
におけるFoo
やlet foo = …;
におけるfoo
は、その名前のもとで新しい何かを導入したという意味で「生成」されたといえます。 一方でfn foo(_: Foo) {}
におけるFoo
やfoo + 3
におけるfoo
は、既存の何かを参照するという意味で「利用」だといえます。
例を挙げて説明するのが一番でしょう。
make_local
という構文拡張があって、let local = 0;
に展開されるとしましょう。これは local
という識別子を生成します。
次のようなコード片を考えます:
make_local!();
assert_eq!(local, 0);
もし assert_eq!(local, 0);
における local
が、この構文拡張が定義したローカル変数に解決されるならば、この構文拡張は(少なくともローカルな名前・束縛(bindings)に関していえば)衛生的ではないということになります。
今度は use_local
という構文拡張があって、local = 42;
に展開されるとしましょう。これは local
という識別子を利用します。
次のようなコード片を考えます:
let mut local = 0;
use_local!();
もし呼び出された構文拡張の内部の local
が、その呼び出しの前に定義されたローカル変数に解決されるのであれば、この構文拡張はやはり衛生的ではないということになります。
以上は衛生性の一般概念への比較的短い導入です。
衛生性については、 macro_rules!
の衛生性と手続き的マクロの衛生性の章で、各々に固有の性質に言及しながら、より深いところまで説明します。
デバッグ
rustc
は構文拡張全般をデバッグするためのツールをいくつか提供しています。さらに、宣言的マクロと手続き的マクロのそれぞれに合わせたより特化したツールも提供します。
普段は展開後のコードを見ることはないため、構文拡張の展開結果がよく分からなくなることがあります。
ありがたいことに、rustc
のunstableな -Zunpretty=expanded
引数を使って展開後のコードを見ることができます。
次のようなコードがあるとします:
// Shorthand for initializing a `String`.
// `String` 初期化の略記法
macro_rules! S {
($e:expr) => {String::from($e)};
}
fn main() {
let world = S!("World");
println!("Hello, {}!", world);
}
これを次のコマンドでコンパイルすると:
rustc +nightly -Zunpretty=expanded hello.rs
次のような結果が得られます(結果を整形しています):
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2018::*;
#[macro_use]
extern crate std;
// Shorthand for initializing a `String`.
// `String` 初期化の略記法
macro_rules! S { ($e : expr) => { String :: from($e) } ; }
fn main() {
let world = String::from("World");
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["Hello, ", "!\n"],
&match (&world,) {
(arg0,) => [
::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt)
],
}
)
);
};
}
構文拡張のデバッグを支援する手段を提供しているのは rustc
だけではありません。
dtolnay氏がcargo-expand
という名前の素晴らしい cargo
プラグインを制作しています。これは基本的には前述した -Zunpretty=expanded
オプションの単なるラッパーです。
Playgroundを利用することもできます。右上にある TOOLS
ボタンをクリックすると、構文拡張を展開するオプションが選択できます。
宣言的マクロ(Declarative Macros)
本章ではRustの宣言的マクロシステム: macro_rules!
について説明します。
前者はこのマクロシステムがどのように動作するのかについて完全かつ徹底的に説明する試みです。一方、後者ではより実践的な例を取り上げます。 体系的説明はこのシステムに関する全ての説明を求める読者向けとなっている一方で、実践的説明はひとつのマクロを実装する体験を通して読者を導く構成となっています。
2種類の説明の補足として、多機能なマクロを実装する際に一般的に非常に役立ついくつかのパターンと構成要素 (building blocks) を紹介します。
宣言的マクロに関する他の資料としては、より敷居が低く高水準な視点からの説明であるThe Rust Book のマクロの章1や、より厳密な詳細に深入りするリファレンス2のマクロの章 が挙げられます。
Note: 本書では、
macro-rules!
によって定義されるマクロを指して MBE(Macro-By-Example), MBEマクロ あるいはmacro-rules!
マクロという用語を用います。
訳注: 日本語版はこちら
訳注: The Rust Referenceのこと
マクロ: 体系的説明
本章ではRustの宣言的なMacro-By-Exampleのシステムについて、全体の概略を説明することによって紹介していきます。 まず構成要素の文法と鍵となるパーツについて説明したあと、最低限知っておくべきより全般的な情報を補足します。
macro_rules!
以上のことを念頭に置いて、macro_rules!
自体の説明に入ります。
先述したとおり、macro_rules!
はそれ自体が構文拡張のひとつであり、これはmacro_rules!
が技術的にはRustの文法に含まれないということを意味します。
macro_rules! $name {
$rule0 ;
$rule1 ;
// …
$ruleN ;
}
少なくとも1つのルールが必要で、最後のルールの後ろのセミコロンは省略できます。
かっこは、角かっこ([]
)、丸かっこ(()
)、波かっこ({}
)のどれを使ってもかまいません。
それぞれの「ルール」は次のような見た目になります:
($マッチパターン) => {$展開形}
先程と同様かっこの種類はどれでもかまいませんが、マッチパターンは丸かっこで、展開形は波かっこで囲む慣習があります。
ここでどのかっこを選ぶかは、MBEマクロの呼び出し方には影響しないということを指摘しておきます。
実際のところ、関数形式マクロはどの種類のかっこを使っても呼び出せます。ただし、{ .. }
や( ... );
(末尾のセミコロンに注意)という形の呼び出しは、常にアイテムとしてパースされるという点で特別です。
不思議に思われるかもしれませんが、macro_rules!
自体の呼び出しは… 何にも展開されません。
少なくとも、抽象構文木には何の変化も起きません。むしろ、その呼び出しはMBEマクロを登録するためにコンパイラ内部のデータ構造を操作します。
そういうわけで、技術的には macro_rules!
は空の展開が許される全ての場所で利用できます。
マッチング
macro_rules!
マクロが呼び出されると、macro_rules!
のインタプリタはルールを1つ1つ定義順に調べていきます。
各ルールについて、インタプリタは入力トークン木の内容とルールの「マッチパターン」のマッチングを試みます。
マッチパターンが入力の全体に一致している場合のみ、マッチしたとみなされます。
入力とマッチパターンがマッチしたら、マクロの呼び出しを「展開形」で置き換えます。マッチしなければ、次のルールを試します。 どのルールにもマッチしなかった場合、展開はエラーとともに失敗します。
最も単純な例は空のマッチパターンです:
macro_rules! four {
() => { 1 + 3 };
}
これは、入力が同様に空のとき、かつそのときに限りマッチします(例: four!()
, four![]
, four!{}
)。
関数形式マクロを呼び出す際に用いるグルーピング用トークン1はマッチ対象ではないことに注意してください。実際のところ、それらのトークンはそもそも入力として渡されません。
つまり、上記のマクロをfour![]
のように呼び出しても、やはりマッチします。
入力トークン木の中身だけが考慮されるということです。
訳注: マクロ呼び出しにおける一番外側のかっこのこと。
マッチパターンには生の(literal)トークン木を含めることもできます。生のトークン木にマッチさせるには、入力が厳密に一致している必要があります。
これを行うには、単にトークン木をそのまま書けばよいです。
例えば、4 fn ['spang "whammo"] @_@
という並びにマッチさせたければ、次のように書けます:
macro_rules! gibberish {
(4 fn ['spang "whammo"] @_@) => {...};
}
トークン木は、書けるものならなんでも使えます。
メタ変数 (Metavariables)
マッチパターンはキャプチャを含むこともできます。 キャプチャは、入力に対する概括的な文法上のカテゴリに基づくマッチングを可能にします。その結果はメタ変数(metavariable)に捕捉され、出力においてメタ変数を捕捉された中身に置換することができます。
キャプチャはドル記号($
)に続けて識別子、コロン(:
)、そしてフラグメント指定子(fragment-specifier)とも呼ばれるキャプチャの種類という形で書きます。フラグメント指定子は以下のどれかでなければなりません:
block
: ブロック (波かっこで囲まれた、文や式からなるかたまり)expr
: 式 (expression)ident
: 識別子 (予約語を含む)item
: アイテム (関数、構造体、モジュール、implなど)lifetime
: ライフタイム (例:'foo
,'static
, ...)literal
: リテラル (例:"Hello World!"
,3.14
,'🦀'
, ...)meta
: メタアイテム。#[...]
や#![...]
といった属性の中にくるものpat
: パターンpath
: パス (path) (例:foo
,::std::mem::replace
,transmute::<_, int>
, …)stmt
: 文 (statement)tt
: 単一のトークン木ty
: 型vis
: 可視性修飾子(visibility qualifier)。空でもよい (例:pub
,pub(in crate)
, ...)
フラグメント指定子のより詳しい説明を見るには、フラグメント指定子の章を参照してください。
例えば、以下のmacro_rules!
マクロは、入力を式として$e
という名前のメタ変数にキャプチャします:
macro_rules! one_expression {
($e:expr) => {...};
}
これらのメタ変数はRustコンパイラのパーサを活用しており、そのため常に「正しい」ことが保証されています。
expr
のメタ変数は常に、コンパイル時のRustバーションにおける完全かつ妥当な式をキャプチャします。
生のトークン木とメタ変数を組み合わせて使うこともできますが、一定の制限(メタ変数と展開・再考で説明します)があります。
メタ変数を参照するには単に$name
と書きます。変数の型はマッチパターンの中で指定済みのため書く必要はありません。例えば次のようになります:
macro_rules! times_five {
($e:expr) => { 5 * $e };
}
マクロの展開と同様に、メタ変数は完全な抽象構文木のノードとして置換されます。
これは、たとえメタ変数$e
にどんなトークンが捕捉されているとしても、単一の完全な式として解釈されるということを意味します。
一つのマッチパターンに複数のメタ変数を書くことができます:
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => { $a * ($b + $c) };
}
また、一つのメタ変数を展開形の中で何度でも使うことができます:
macro_rules! discard {
($e:expr) => {};
}
macro_rules! repeat {
($e:expr) => { $e; $e; $e; };
}
さらに、$crate
という特別なメタ変数があり、現在のクレートを参照するのに使えます。
繰り返し
マッチパターンは「繰り返し」を含むことができます。これによりトークンの列へのマッチングが可能になります。
繰り返しは $ ( ... ) sep rep
という一般形式を持ちます。
-
$
は文字通りのドル記号のトークン。 -
( ... )
は繰り返し対象となるかっこで括られたマッチパターン。 -
sep
は省略可能な区切りのトークン。区切り文字(delimiter)2や繰り返し演算子は使えない。よく使われるのは,
や;
。 -
rep
は必須の繰り返し演算子。現時点で以下のものが使える:?
: 最大1回の繰り返し*
: 0回以上の繰り返し+
: 1回以上の繰り返し
?
は最大1回の出現を表すので、区切りトークンと一緒に使うことはできない。
訳注: ここでの区切り文字(delimiter)は、いわゆる「かっこ」に使われる文字: ( ) [ ] { }
を指す。
繰り返しの中では、生のトークン木、メタ変数、任意にネストした他の繰り返しを含む、任意の正当なマッチパターンを使えます。
繰り返しは展開形の中でも同じ構文を用います。繰り返されるメタ変数は展開形の中の繰り返しの内部からしかアクセスできません。
例えば、以下は各要素を文字列にフォーマットするMBEマクロです。 0個以上のコンマ区切りの式にマッチし、ベクタを生成する式に展開されます。
macro_rules! vec_strs { ( // Start a repetition: // 繰り返しの開始: $( // Each repeat must contain an expression... // 各繰り返しは式を含み... $element:expr ) // ...separated by commas... // ...コンマで区切られ... , // ...zero or more times. // ...0回以上繰り返される * ) => { // Enclose the expansion in a block so that we can use // multiple statements. // 複数の式を使うため、展開形をブロックで囲む { let mut v = Vec::new(); // Start a repetition: // 繰り返しの開始: $( // Each repeat will contain the following statement, with // $element replaced with the corresponding expression. // 各繰り返しは次のような文に展開される。ここで $element は対応する式に置き換えられる v.push(format!("{}", $element)); )* v } }; } fn main() { let s = vec_strs![1, "a", true, 3.14159f32]; assert_eq!(s, &["1", "a", "true", "3.14159"]); }
すべてのメタ変数が同じ回数だけ繰り返される場合に限り、一つの繰り返しの中で複数のメタ変数を繰り返すことができます。 よって、次のようなマクロ呼び出しは動作します:
#![allow(unused)] fn main() { macro_rules! repeat_two { ($($i:ident)*, $($i2:ident)*) => { $( let $i: (); let $i2: (); )* } } repeat_two!( a b c d e f, u v w x y z ); }
しかしこれは動作しません:
#![allow(unused)] fn main() { macro_rules! repeat_two { ($($i:ident)*, $($i2:ident)*) => { $( let $i: (); let $i2: (); )* } } repeat_two!( a b c d e f, x y z ); }
これは次のようなエラーで失敗します。
error: meta-variable `i` repeats 6 times, but `i2` repeats 3 times
--> src/main.rs:6:10
|
6 | $( let $i: (); let $i2: (); )*
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
メタ変数式(Metavariable Expressions)
RFC: rfcs#1584
トラッキングIssue: rust#83527
機能フラグ:#![feature(macro_metavar_expr)]
マクロの書き換え先(transcriber)には、メタ変数式(metavariable expressions)と呼ばれるものを書くことができます。
メタ変数式は書き換え先に対し、他の方法では簡単に得られないメタ変数に関する情報を提供します。
$$
式という例外を除き、メタ変数式は $ { op(...) }
という一般形式を持ちます。
現時点で $$
式を除く全てのメタ変数式が繰り返しに対応しています。
以下のような式が利用できます。ここで、ident
は束縛済みメタ変数の名前、depth
は整数リテラルです:
${count(ident)}
: 最も内側の繰り返しにおける$ident
の繰り返し回数。${count(ident, 0)}
と等価。${count(ident, depth)}
:depth
の深さにある繰り返しにおける$ident
の繰り返し回数。${index()}
: 最も内側の繰り返しにおける、現在の繰り返しインデックス。${index(0)}
と等価。${index(depth)}
: 深さdepth
にある繰り返しにおける、現在の繰り返しインデックス。深さは内側から外側に向かって数える。${length()}
: 最も内側の繰り返しが繰り返される回数。${length(0)}
と等価。${length(depth)}
: 深さdepth
にある繰り返しが繰り返される回数。深さは内側から外側に向かって数える。${ignore(ident)}
: 繰り返しのために$ident
を束縛するが、何にも展開しない。$$
: 単一の$
記号に展開される。実質的に、$
が書き換わらないようエスケープする。
メタ変数式の完全な文法定義については、RustリファレンスのMacros By Exampleの章を参照してください。
マクロ: 実践的説明
本章では、Rustの宣言的なMacro-By-Exampleのシステムについて、比較的シンプルで実践的な例を通して説明していきます。
高水準な視点からの説明としては、他にもThe Rust Bookのマクロの章があります。 また本書の形式的説明の章では、このマクロシステムについて詳細に説明しています。
背景を少し
Note: 落ち着いて! これに続くのはマクロの説明に関係するちょっとした数学の話です。 早くこの章の本題に入りたいのであれば、この節を飛ばして読んでも大丈夫です。
詳しくない方向けに説明すると、漸化式とは、各値が1つ以上前の値に基づいて定まる数列で、全ての始まりである1つ以上の初期値を伴います。 例えば、フィボナッチ数列1は次の漸化式により定義されます:
\[F_{n} = 0, 1, ..., F_{n-2} + F_{n-1}\]
訳注: 日本語版はこちら。
したがって、数列の最初の2つの数は 0 と 1、3番めは \( F_{0} + F_{1} = 0 + 1 = 1\)、 4番めは \( F_{1} + F_{2} = 1 + 1 = 2\)、という具合に無限に続きます。
さて、このような数列は無限に続くため、fibonacci
関数を定義するのは少しややこしい作業になります。というのも、明らかに完全なベクタを返すべきではないからです。
ここですべきことは、必要に応じて数列の要素を遅延的に計算する何かを返すことです。
Rustにおいて、これはIterator
を生成せよ、ということです。
これは特別難しいことではありませんが、かなりの量のボイラープレートを必要とします。独自の型を定義し、その型に保存すべき状態を考え出し、Iterator
トレイトを実装する必要があります。
ですが、小さなmacro_rules!
マクロに基づくコード生成だけでこれらの詳細のほとんどを括りだすことができるくらい、漸化式はシンプルです。
それでは、以上のことを踏まえて、早速始めていきましょう。
構成要素
たいてい、新しい macro_rules!
マクロの実装に取りかかるとき、私が初めにするのはその呼び出し方を決めることです。
今回のケースでは、最初の試行は次のようなものになりました:
let fib = recurrence![a[n] = 0, 1, ..., a[n-2] + a[n-1]];
for e in fib.take(10) { println!("{}", e) }
これをもとに、実際の展開形について確信は持てなくとも、macro_rules!
マクロがどのように定義されるべきかを考えてみることはできます。
入力の構文をパースする方法を思いつけないのであれば、構文を変更する必要があるかもしれないということなので、これは有用な考え方です。
macro_rules! recurrence {
( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}
fn main() {}
この構文は見慣れないものだと思いますので、少し説明させてください。
これは recurrence!
という名前の、macro_rules!
のシステムを使った構文拡張の定義になります。
この macro_rules!
マクロはただ一つの構文ルールを持っています。
そのルールは、呼び出しの入力が次のものに一致しなければならないというものです:
- リテラルトークンの列
a
[
n
]
=
,
を区切りとする、1回以上 (+
) の妥当な式の繰り返し ($( ... )
)。この式はメタ変数inits
に捕捉される ($inits:expr
)- リテラルトークンの列
,
...
,
- 妥当な式。この式はメタ変数
recur
に捕捉される ($recur:expr
)
結局、このルールは、もし入力がこのルールに一致したら、マクロの呼び出しを /* ... */
というトークンの列で置き換えよ、ということを表しています。
inits
は、その名前が示唆するように、最初や最後だけではなく、その位置にあるすべての式を含むことに注意してください。
さらにいえば、inits
は、それらの式を不可逆的にまとめてペーストするような形ではなく、列として捕捉します。
また、+
の代わりに *
を使えば「0回以上」の繰り返しを、?
を使えば「任意」、つまり「0回か1回」の繰り返しを表せます。
練習として、先に挙げた入力をこのルールに与えてみて、どのように処理されるか見てみましょう。
「位置」欄では、次に構文パターンのどの部分がマッチングされるかを「 ⌂ 」で示しています。
ある状況では、マッチング対象となる「次」の要素の候補が複数存在することがあるのに注意してください。
「入力」欄は、まだ消費されていないトークンです。
inits
・recur
欄はそれらに捕捉されている内容です。
位置 | 入力 | inits |
recur |
---|---|---|---|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
a[n] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
[n] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
n] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
= 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
, 1, ..., a[n-2] + a[n-1] |
0 |
|
Note: ここには2つの ⌂ がある。これは次の入力トークンが、繰り返しの要素間のコンマ区切りか、繰り返しの後のコンマのどちらかにマッチしうるため。 マクロシステムは、どちらに従うべきかが確定するまでの間、両方の可能性を追跡する。 | |||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
1, ..., a[n-2] + a[n-1] |
0 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
, ..., a[n-2] + a[n-1] |
0 , 1 |
|
Note: 3つめの取り消し線つきのマーカーは、最後のトークンの消費の結果、マクロシステムがありうる選択肢の1つをふるい落としたことを表す。 | |||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
..., a[n-2] + a[n-1] |
0 , 1 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
|
, a[n-2] + a[n-1] |
0 , 1 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
a[n-2] + a[n-1] |
0 , 1 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
0 , 1 |
a[n-2] + a[n-1] |
|
Note: このステップは、コンパイラが持つ「妥当な式の構成要素」に関する知識を用いて、$recur:exprのような束縛が式全体を消費することを明確にする。 後述するように、他の言語要素に対してもこれを行うことができる。 |
ここで重要なのは、マクロシステムが、入力として与えられたトークンたちを所与のルールに対してインクリメンタルにマッチングを試みるということです。 「試みる」という部分については後で補足します。
さて、最後の、完全に展開された形を書きはじめましょう。 この展開に対しては、次のようなものが求められています:
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
これは実際のイテレータ型になるべきものです。
mem
は漸化式を計算するのに必要となる、直近の数個の値を保持するメモバッファになります。
pos
は n
の値を追跡するための変数です。
余談:
u64
は、数列の要素を表すのに「十分大きな」型として選びました。 これが他の数列に対して上手くいくかを心配する必要はありません。きっと上手くいきますよ。
impl Iterator for Recurrence {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
数列の初期値を生成する分岐が必要です。難しいところはないでしょう。
} else {
let a = /* something */;
let n = self.pos;
let next_val = a[n-2] + a[n-1];
self.mem.TODO_shuffle_down_and_append(next_val);
self.pos += 1;
Some(next_val)
}
}
}
こちらはちょっと難しいです。a
を厳密にどう定義するかについては、あとで見ていきます。
TODO_shuffle_down_and_append
も仮実装になっています。
ここには、next_val
を配列の末尾に配置し、残りの要素を1つずつずらし、最初の要素を削除するものが必要です。
Recurrence { mem: [0, 1], pos: 0 }
};
for e in fib.take(10) { println!("{}", e) }
最後に、この新しい構造体のインスタンスを返します。これに対して反復処理を行うことができます。 まとめると、展開形の全容は以下のようになります:
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
impl Iterator for Recurrence {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let a = /* something */;
let n = self.pos;
let next_val = (a[n-2] + a[n-1]);
self.mem.TODO_shuffle_down_and_append(next_val.clone());
self.pos += 1;
Some(next_val)
}
}
}
Recurrence { mem: [0, 1], pos: 0 }
};
for e in fib.take(10) { println!("{}", e) }
余談: そう、これはマクロの呼び出しのたびに別の
Recurrence
構造体とその実装を定義することを意味します。 ほとんどの部分は最終的なバイナリ上では最適化されるでしょう。
展開形を書きながら、それを見直すのも有用です。
展開形の中に、呼び出しのたびに異なるべき何かがあって、それがマクロが実際に受け入れる構文の中にないのであれば、それをどこに導入するか考える必要があります。
今回の例では、u64
を追加しましたが、それはユーザにとってもマクロ構文中にも必ずしも必要なものではありません。修正しましょう。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; } /* let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } */ fn main() {}
新たにメタ変数 sty
を追加しました。これは型にマッチします。
余談: メタ変数のコロン以降の部分は、マッチする構文の種類を表します。 よく使われるのは
item
,expr
, そしてty
です。 詳しい説明は 「マクロ: 形式的説明」 の章の 「メタ変数」の項目をご覧ください。もう一つ知っておくべきことがあります。言語の将来の変化に備える意味で、コンパイラはマッチパターンの種類に応じて、そのあとに続けられるトークンの種類に制限を設けています。 概して、これは式や文にマッチングさせようとしたときに問題になります。 これらのあとに続けられるのは
=>
,,
,;
のみとなります。完全なリストは「枝葉末節」の章の「メタ変数と展開・再考」の節にあります。
添字付け (indexing) と入れ替え
マクロの話からそれることになるので、ここはさらっと流そうと思います。
a
に添字にアクセス機能をつけることで、ユーザが数列の前のほうの値にアクセスできるようにしたいです。
これは、数列の直近の数個(今回の例では2個)の要素を保持するスライディングウィンドウのように動きます。
ラッパー型によって、いとも簡単にこれを実現できます:
struct IndexOffset<'a> {
slice: &'a [u64; 2],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(2);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
余談: Rust初心者にとっては多すぎる数のライフタイムが出てきたので、簡単に説明しましょう。
'a
や'b
はライフタイムパラメータといい、参照(何らかのデータを指す借用されたポインタ)が有効な範囲を追跡するのに使われます。 今回、IndexOffset
は我々のイテレータのデータへの参照を借用しているので、'a
を用いてIndexOffset
がその参照をいつまで保持できるかを追跡する必要があります。
'b
が用いられているのは、Index::index
関数 (添字記法 (subscript syntax) の実装本体) もまた、借用された参照を返すためにライフタイムによってパラメータ化されているためです。'a
と'b'
が常に同じである必要はありません。 借用チェッカーは、我々が明示的に'a
と'b
をお互いと関連付けなくても、我々が誤ってメモリ安全性を侵害していないことを確かめてくれます。
これにより、a
の定義は次のように変わります:
let a = IndexOffset { slice: &self.mem, offset: n };
唯一未解決なのは、TODO_shuffle_down_and_append
をどうすべきかということです。
標準ライブラリの中にそのものズバリの機能を持つメソッドは見つかりませんでしたが、自分で書くするのは特に難しくありません。
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in (0..2).rev() {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}
これは新しい値を配列の末尾要素と入れ替え、他の要素を1つずつ前に入れ替えていきます。
余談: このような方法をとることで、このコードはコピーできない型に対しても動作します。
現時点における、動くコードは以下のようになります:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; } fn main() { /* let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } */ let fib = { use std::ops::Index; struct Recurrence { mem: [u64; 2], pos: usize, } struct IndexOffset<'a> { slice: &'a [u64; 2], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = u64; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b u64 { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(2); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = u64; #[inline] fn next(&mut self) -> Option<u64> { if self.pos < 2 { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; a[n-2] + a[n-1] }; { use std::mem::swap; let mut swap_tmp = next_val; for i in [1,0] { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [0, 1], pos: 0 } }; for e in fib.take(10) { println!("{}", e) } }
n
と a
の宣言の順序が入れ替わっており、さらにそれらが(漸化式の計算式と一緒に)ブロックで囲まれていることに注意してください。
前者の理由は明白でしょう(n
を a
の初期化で使うため)。
後者の理由は、参照の借用 &self.mem
が、その後の入れ替え処理の実行を妨げてしまうためです(別の場所で借用された値を変更することはできません)。
このブロックにより、&self.mem
の借用は入れ替え処理よりも前に失効するようになります。
ちなみに、mem
swap を実行するコードをブロックで囲んでいるのは、コードの整頓の目的で、std::mem::swap
が使えるスコープを限定するためでしかありません。
このコードを実行すると、次の結果が得られます:
0
1
1
2
3
5
8
13
21
34
成功です! さて、これをマクロの展開形の部分にコピー & ペーストして、展開結果のコードをマクロの呼び出しに置き換えてみましょう。 次のようになります:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { { /* What follows here is *literally* the code from before, cut and pasted into a new position. No other changes have been made. */ use std::ops::Index; struct Recurrence { mem: [u64; 2], pos: usize, } struct IndexOffset<'a> { slice: &'a [u64; 2], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = u64; fn index<'b>(&'b self, index: usize) -> &'b u64 { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(2); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = u64; fn next(&mut self) -> Option<u64> { if self.pos < 2 { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; (a[n-2] + a[n-1]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..2).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [0, 1], pos: 0 } } }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
明らかに、まだメタ変数を使っていませんが、メタ変数を使う形に変更するのはとても簡単です。
しかし、これをコンパイルしようとすると、rustc
は次のような文句を言って中断します:
error: local ambiguity: multiple parsing options: built-in NTs expr ('inits') or 1 other option.
--> src/main.rs:75:45
|
75 | let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-2] + a[n-1]];
|
ここで、我々は macro_rules!
システムの限界に達してしまいました。
問題となるのは2つめのコンマです。
展開中にそのコンマを見た段階で、macro_rules!
は次に inits
のためのもう一つの式と、...
のどちらをパースすべきかを決めることができないのです。
悲しいことに、...
が妥当な式ではないと気づけるほど macro_rules!
システムは賢くないので、諦めてしまいます。
理論的には、これは思ったとおりに動作すべきですが、現時点ではそうなっていません。
余談: 我々のルールがマクロシステムによってどのように解釈されるかについて、私は少し嘘をつきました。 一般に、それは書いたように動くべきですが、今回のような場合は動きません。 現状、
macro_rules
の機構には弱点があり、うまく動くようにするためには形を少し歪める必要がある、ということを折に触れて思い出すとよいでしょう。今回の例においては、2つの問題があります。 1つめは、マクロシステムが、多種多様な文法要素(例: 式)について、何が構成要素となり、何が構成要素となりえないのかに関する知識を持たないということです。これはパーサの仕事なのです。 そのため、マクロシステムは
...
が式になりえないことを知りません。 2つめは、(式のような)複合的な文法要素を捕捉しようとするには、それに100%身を捧げるしかないということです。言い換えると、マクロシステムはパーサに何らかの入力を式としてパースするよう依頼することができますが、パーサは任意の問題に対して「中断」という形で応える、ということです。 現状、マクロシステムがこれに対処するための唯一の方法は、それが問題になるような状況を禁じることだけです。
明るい面を挙げるとすれば、誰もこの事態について躍起にはなっていないということです。 より綿密に定義された、未来のマクロシステムのために、
macro
というキーワードがすでに予約されています。 これが使えるようになるまでは、やりたくなくてもそうするしかありません。
ありがたいことに、修正は比較的シンプルに済みます。構文からコンマを取り除くのです。
バランスを取るために、...
の両側のコンマを取り除きましょう:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { // ^~~ changed /* ... */ // Cheat :D (vec![0u64, 1, 2, 3, 5, 8, 13, 21, 34]).into_iter() }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-2] + a[n-1]]; // ^~~ changed for e in fib.take(10) { println!("{}", e) } }
やったか!?と思いきや…
以前は問題なかったにもかかわらず、これはコンパイラによって拒否されてしまいます。
理由は、コンパイラは今 ...
をトークンとして認識するようになり、ご存知のように式フラグメントの後ろでは =>
, ,
または ;
しか使えないためです。
よって、残念ながら我々が夢見た構文は動作しません。運は尽きました。代わりに使える中で、最も相応しいものを選びましょう。,
を ;
に書き換えます。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { // ^~~~~~^ changed /* ... */ // Cheat :D (vec![0u64, 1, 2, 3, 5, 8, 13, 21, 34]).into_iter() }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-2] + a[n-1]]; // ^~~~~^ changed for e in fib.take(10) { println!("{}", e) } }
やりました!今回は本当に成功です。
置換
マクロによって捕捉したものを使って置換を行うのはとても簡単です。メタ変数 $sty:ty
の中身を $ty
を使って挿入できます。
では、u64
を直していきましょう:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { { use std::ops::Index; struct Recurrence { mem: [$sty; 2], // ^~~~ changed pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; 2], // ^~~~ changed offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; // ^~~~ changed #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { // ^~~~ changed use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(2); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; // ^~~~ changed #[inline] fn next(&mut self) -> Option<$sty> { // ^~~~ changed /* ... */ if self.pos < 2 { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; (a[n-2] + a[n-1]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..2).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [0, 1], pos: 0 } } }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
もっと難しいことに挑戦してみましょう。inits
を配列リテラル [0, 1]
と 配列の型 [$sty; 2]
の両方に変換するにはどうすればよいでしょう。
最初にできるのはこんな感じのことです:
Recurrence { mem: [$($inits),+], pos: 0 }
// ^~~~~~~~~~~ changed
これは実質的にキャプチャと逆のことをしています。コンマで区切りつつ、inits
を1回以上繰り返すのです。
これは期待されているトークン列 0, 1
に展開されます。
inits
を リテラル 2
にするのは少し大変そうです。
結局のところこれを直接行う方法はないのですが、もう一つの macro_rules!
マクロを使えば可能です。
一歩ずつ進んでいきましょう。
macro_rules! count_exprs { /* ??? */ () => {} } fn main() {}
自明なケースである、0個の式が与えられたときは、 count_exprs
は リテラル 0
に展開されるべきです。
macro_rules! count_exprs { () => (0); // ^~~~~~~~~~ added } fn main() { const _0: usize = count_exprs!(); assert_eq!(_0, 0); }
余談: 式を囲むために、波かっこの代わりに丸かっこを使ったことに気づいた方がいるかもしれません。
macro_rules
は、かっこが一致している限りは、どのかっこを使おうがまったく気にしません。 実際、マクロ自体のかっこ(マクロ名のすぐ右にあるもの)、構文ルールを囲むかっこ、そしてそれに対応する展開形を囲むかっこを好きに切り替えることができます。マクロを呼び出す際に使うかっこを切り替えることもできますが、この場合少し制限が強くなります。
{ ... }
または( ... );
という形で呼び出されたマクロは常にアイテム(struct
やfn
の宣言のようなもの)としてパースされます。 これはマクロを関数の本体の中で使うときに重要になります。「式のようにパース」するか「文のようにパース」するかをはっきりさせるのに役立ちます。
式が1つの場合はどうでしょうか?
それはリテラル 1
に展開されるべきです。
macro_rules! count_exprs { () => (0); ($e:expr) => (1); // ^~~~~~~~~~~~~~~~~ added } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); assert_eq!(_0, 0); assert_eq!(_1, 1); }
2つなら?
macro_rules! count_exprs { () => (0); ($e:expr) => (1); ($e0:expr, $e1:expr) => (2); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ added } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); }
式が2つの場合を再帰的に表しなおすことで、これを「単純化」できます。
macro_rules! count_exprs { () => (0); ($e:expr) => (1); ($e0:expr, $e1:expr) => (1 + count_exprs!($e1)); // ^~~~~~~~~~~~~~~~~~~~~ changed } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); }
Rustは 1 + 1
を定数値に畳み込むので、問題ありません。
式が3つならどうでしょうか?
macro_rules! count_exprs { () => (0); ($e:expr) => (1); ($e0:expr, $e1:expr) => (1 + count_exprs!($e1)); ($e0:expr, $e1:expr, $e2:expr) => (1 + count_exprs!($e1, $e2)); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); const _3: usize = count_exprs!(x, y, z); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); assert_eq!(_3, 3); }
余談: もしルールの順序を逆にしたとしても問題ないのだろうか、と思った方がいるかもしれません。 今回の例に限っていえば、問題ありません。しかし、マクロシステムは時にルールの順序にうるさくなり、なかなか言うことを聞かなくなることがあります。 間違いなく動くはずだと思っていた複数のルールを持つマクロが、「予期しないトークン(unexpected tokens)」というエラーを出すようであれば、ルールの順序を変えてみましょう。
パターンが見えてきたのではないでしょうか。 1つの式とそれに続く0個以上の式にマッチングさせ、1 + (残りのカウント) の形に展開することで、式のリストを畳み込む(reduce)ことができます。
macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ changed } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); const _3: usize = count_exprs!(x, y, z); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); assert_eq!(_3, 3); }
JFTE: これはものを数えるための唯一の方法でも、最良の方法でもありません。 より効率的な方法を知るには、数を数えるの節をよく読むとよいでしょう。
これを使って、recurrence
を変更し、mem
に必要なサイズを割り出すようにできます。
// added: macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { { use std::ops::Index; const MEM_SIZE: usize = count_exprs!($($inits),+); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added struct Recurrence { mem: [$sty; MEM_SIZE], // ^~~~~~~~ changed pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], // ^~~~~~~~ changed offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(MEM_SIZE); // ^~~~~~~~ changed let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; #[inline] fn next(&mut self) -> Option<$sty> { if self.pos < MEM_SIZE { // ^~~~~~~~ changed let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; (a[n-2] + a[n-1]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { // ^~~~~~~~ changed swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [$($inits),+], pos: 0 } } }; } /* ... */ fn main() { let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
inits
の置換はこれで完成したので、ついに最後の recur
式の置換に移れます。
macro_rules! count_exprs { () => (0); ($head:expr $(, $tail:expr)*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { { use std::ops::Index; const MEM_SIZE: usize = count_exprs!($($inits),+); struct Recurrence { mem: [$sty; MEM_SIZE], pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(MEM_SIZE); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; /* ... */ #[inline] fn next(&mut self) -> Option<u64> { if self.pos < MEM_SIZE { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; $recur // ^~~~~~ changed }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } /* ... */ } Recurrence { mem: [$($inits),+], pos: 0 } } }; } fn main() { let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
そして、完成した macro_rules!
マクロをコンパイルすると...
error[E0425]: cannot find value `a` in this scope
--> src/main.rs:68:50
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
error[E0425]: cannot find value `n` in this scope
--> src/main.rs:68:52
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
error[E0425]: cannot find value `a` in this scope
--> src/main.rs:68:59
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
error[E0425]: cannot find value `n` in this scope
--> src/main.rs:68:61
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
... 待て、何だって? そんなはずは... マクロがどう展開されているか確かめてみましょう。
$ rustc +nightly -Zunpretty=expanded recurrence.rs
-Zunpretty=expanded
引数は rustc
にマクロの展開を行うように伝え、それから結果の抽象構文木をソースコードに戻します。
出力(フォーマット整理済)を以下に示します。
特に、$recur
が置換された箇所に注意してみましょう。
#![feature(no_std)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
fn main() {
let fib = {
use std::ops::Index;
const MEM_SIZE: usize = 1 + 1;
struct Recurrence {
mem: [u64; MEM_SIZE],
pos: usize,
}
struct IndexOffset<'a> {
slice: &'a [u64; MEM_SIZE],
offset: usize,
}
impl <'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
#[inline(always)]
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(MEM_SIZE);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < MEM_SIZE {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = {
let n = self.pos;
let a = IndexOffset{slice: &self.mem, offset: n,};
a[n - 1] + a[n - 2]
};
{
use std::mem::swap;
let mut swap_tmp = next_val;
{
let result =
match ::std::iter::IntoIterator::into_iter((0..MEM_SIZE).rev()) {
mut iter => loop {
match ::std::iter::Iterator::next(&mut iter) {
::std::option::Option::Some(i) => {
swap(&mut swap_tmp, &mut self.mem[i]);
}
::std::option::Option::None => break,
}
},
};
result
}
}
self.pos += 1;
Some(next_val)
}
}
}
Recurrence{mem: [0, 1], pos: 0,}
};
{
let result =
match ::std::iter::IntoIterator::into_iter(fib.take(10)) {
mut iter => loop {
match ::std::iter::Iterator::next(&mut iter) {
::std::option::Option::Some(e) => {
::std::io::_print(::std::fmt::Arguments::new_v1(
{
static __STATIC_FMTSTR: &'static [&'static str] = &["", "\n"];
__STATIC_FMTSTR
},
&match (&e,) {
(__arg0,) => [::std::fmt::ArgumentV1::new(__arg0, ::std::fmt::Display::fmt)],
}
))
}
::std::option::Option::None => break,
}
},
};
result
}
}
それでもやはり問題ないように見えます!
いくつか不足している #![feature(...)]
属性を追加して、rustc
のnightlyビルドに食わせると、なんとコンパイルが通ります! ... 何だって?!
余談: 上のコードはnightlyビルドでない
rustc
ではコンパイルできません。 これはprintln!
マクロの展開形が、公に標準化されていないコンパイラの内部詳細に依存しているためです。
衛生的に行こう
ここでの問題は、Rustの構文拡張において識別子が衛生的(hygienic)であるということです。 つまり、2つの別のコンテキストからくる識別子は衝突しえないということです。 違いを示すため、もっと単純な例を見てみましょう。
macro_rules! using_a {
($e:expr) => {
{
let a = 42i;
$e
}
}
}
let four = using_a!(a / 10);
fn main() {}
このマクロは単に、式をとって、それを変数 a
が定義されたブロックに包みます。
これを、 4
を計算するための回りくどい方法として使います。
実はこの例には2つの構文コンテキストが含まれていますが、それらは目に見えません。
そこで、見分けがつくように、それぞれのコンテキストに別々の色をつけてみましょう。
まず展開前のコードからいきます。ここにはただ1つのコンテキストがあります。
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);
さて、マクロ呼び出しを展開しましょう。
let four = { let a = 42; a / 10 };
ご覧の通り、マクロ呼び出しの結果定義された a
は、呼び出しに使った a
とは別のコンテキストにあります。
そのため、字句的な見た目が同じにもかかわらず、コンパイラはそれらを完全に別の識別子として扱います。
これは macro_rules!
マクロを扱う際、さらには一般の構文拡張を扱う際に、本当に注意すべきことです。
同じ見た目の抽象構文木であっても、マクロが出力したものはコンパイルに失敗し、手書きや -Zunpretty=expanded
を用いてダンプした結果であればコンパイルに成功する、ということがありうるのです。
これに対する解決策は、識別子を適切な構文コンテキストにおいて捕捉することです。 そのためには、再度マクロの構文を調整する必要があります。 引き続き単純な例で考えます:
macro_rules! using_a {
($a:ident, $e:expr) => {
{
let $a = 42;
$e
}
}
}
let four = using_a!(a, a / 10);
これは次のように展開されます:
let four = { let a = 42; a / 10 };
今度はコンテキストが一致し、このコードはコンパイルに成功します。
a
と n
を明示的に捕捉することで、recurrence!
マクロに対してもこの調整を行うことができます。
必要な変更を加えると、次のようになります:
macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { // ^~~~~~~~~~ ^~~~~~~~~~ changed { use std::ops::Index; const MEM_SIZE: usize = count_exprs!($($inits),+); struct Recurrence { mem: [$sty; MEM_SIZE], pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(MEM_SIZE); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; #[inline] fn next(&mut self) -> Option<$sty> { if self.pos < MEM_SIZE { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let $ind = self.pos; // ^~~~ changed let $seq = IndexOffset { slice: &self.mem, offset: $ind }; // ^~~~ changed $recur }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [$($inits),+], pos: 0 } } }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
そしてこれはコンパイルに成功します! では、別の数列を試してみましょう。
macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { { use std::ops::Index; const MEM_SIZE: usize = count_exprs!($($inits),+); struct Recurrence { mem: [$sty; MEM_SIZE], pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(MEM_SIZE); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; #[inline] fn next(&mut self) -> Option<$sty> { if self.pos < MEM_SIZE { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let $ind = self.pos; let $seq = IndexOffset { slice: &self.mem, offset: $ind }; $recur }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [$($inits),+], pos: 0 } } }; } fn main() { for e in recurrence!(f[i]: f64 = 1.0; ...; f[i-1] * i as f64).take(10) { println!("{}", e) } }
結果はこうなります:
1
1
2
6
24
120
720
5040
40320
362880
成功です!
枝葉末節
本節では macro_rules!
システムのより細かな詳細に踏み込んでいきます。
最低限、これらの詳細や論点について知っておくよう心がけたほうがよいでしょう。
フラグメント指定子 (Frangment Specifiers)
体系的説明の章で触れたように、Rust(1.60時点)には14種類のフラグメント指定子があります。 本節ではその一部についてもう少し詳しく説明し、各マッチパターンに一致する入力例を示します。
Note:
ident
,lifetime
,tt
以外のすべてのフラグメントによる捕捉は、抽象構文木を不透明(opaque)にします。これにより、その抽象構文木は後のマクロ呼び出しにおいてそれ以上フラグメント指定子にマッチしなくなります。
block
block
フラグメントは、開き波かっこ {
・任意の数の文・閉じ波かっこ }
の並びからなるブロック式のみにマッチします。
macro_rules! blocks { ($($block:block)*) => (); } blocks! { {} { let zig; } { 2 } } fn main() {}
expr
expr
フラグメントは、任意の種類の式にマッチします (Rustは「式指向 (expression oriented)」の言語なので、たくさんの種類の式があります)。
macro_rules! expressions { ($($expr:expr)*) => (); } expressions! { "literal" funcall() future.await break 'foo bar } fn main() {}
ident
ident
フラグメントは、識別子または予約語にマッチします。
macro_rules! idents { ($($ident:ident)*) => (); } idents! { // _ <- This is not an ident, it is a pattern // _ <- これは識別子ではなくパターン foo async O_________O _____O_____ } fn main() {}
item
item
フラグメントは、任意のRustのアイテムの定義のみにマッチし、アイテムを参照する識別子にはマッチしません。
これには可視性修飾子を含みます。
macro_rules! items { ($($item:item)*) => (); } items! { struct Foo; enum Bar { Baz } impl Foo {} pub use crate::foo; /*...*/ } fn main() {}
lifetime
lifetime
フラグメントは、ライフタイムとラベルにマッチします。
ライフタイムは、'
が先頭につくことを除きident
と非常に似ています。
macro_rules! lifetimes { ($($lifetime:lifetime)*) => (); } lifetimes! { 'static 'shiv '_ } fn main() {}
literal
literal
フラグメントは、任意のリテラル式にマッチします。
macro_rules! literals { ($($literal:literal)*) => (); } literals! { -1 "hello world" 2.3 b'b' true } fn main() {}
meta
meta
フラグメントは、属性の中身にマッチします。
すなわち、それは単純パス(simple path)と呼ばれるジェネリック引数を持たないパスの後ろに、かっこで括られたトークン木または =
とリテラル式の並びが続いたものにマッチします
Note: 通常、このフラグメントは、属性を捕捉するための #[$meta:meta]
または#![$meta:meta]
といったマッチパターンの中で使われます。
macro_rules! metas { ($($meta:meta)*) => (); } metas! { ASimplePath super::man path = "home" foo(bar) } fn main() {}
Docコメントの真実:
/// ...
や!// ...
のようなDocコメントは、実は属性の構文糖衣なのです! これらはそれぞれ#[doc="..."]
や#![doc="..."]
という形に脱糖されます。 つまり、Docコメントに対して属性と同様のマッチングが行えるということです!
pat
pat
フラグメントは、任意の種類のパターンにマッチします。これには、Rust 2021 エディションで追加されたorパターンを含みます。
macro_rules! patterns { ($($pat:pat)*) => (); } patterns! { "literal" _ 0..5 ref mut PatternsAreNice 0 | 1 | 2 | 3 } fn main() {}
pat_param
Rust 2021 エディションにおいて、orパターンのパースを可能にするために pat
フラグメントの動作が変更されました。
pat
フラグメントに続けられるもののリストが変更され、|
トークンを続けることができなくなりました。
この問題を回避する、または従来の動作を取り戻すのに、pat_param
フラグメントが使えます。これには |
を続けることができますが、一方でトップレベルのorパターンには使えません。
macro_rules! patterns { ($( $( $pat:pat_param )|+ )*) => (); } patterns! { "literal" _ 0..5 ref mut PatternsAreNice 0 | 1 | 2 | 3 } fn main() {}
path
path
フラグメントは、TypePathと呼ばれるスタイルのパスにマッチします。
これには関数スタイルのトレイト Fn() -> ()
を含みます。
macro_rules! paths { ($($path:path)*) => (); } paths! { ASimplePath ::A::B::C::D G::<eneri>::C FnMut(u32) -> () } fn main() {}
stmt
stmt
フラグメントは文のみにマッチします。(Unit構造体のような)セミコロンが必須のアイテム文の場合を除き、末尾のセミコロンにはマッチしません。
簡単な例によって、これが正確には何を意図しているのかを示します。 捕捉したものをそのまま出力するだけのマクロを使います:
macro_rules! statements {
($($stmt:stmt)*) => ($($stmt)*);
}
fn main() {
statements! {
struct Foo;
fn foo() {}
let zig = 3
let zig = 3;
3
3;
if true {} else {}
{}
}
}
例えばplaygroundを使って1、これを展開すると、おおよそ次のようなものが得られます:
/* snip */
fn main() {
struct Foo;
fn foo() { }
let zig = 3;
let zig = 3;
;
3;
3;
;
if true { } else { }
{ }
}
この結果からいくつかのことがいえます。
まず、すぐにわかることは、stmt
フラグメントが末尾のセミコロンを捕捉しないにもかかわらず、必要なところにセミコロンが出力されているということです。これは、すでに文の末尾にセミコロンがあったとしても同様です。
この理由は単純で、セミコロンはそれ自体がすでに、stmt
フラグメントが捕捉する妥当な文だからです。
よって、このマクロは8回ではなく10回捕捉を行っているのです!
これは複数回の繰り返しを行い、それらを一つの繰り返し展開形として展開する際に重要となりえます。そのようなケースでは繰り返しの回数が一致している必要があるためです。
もう一つわかることは、アイテム文struct Foo;
の末尾のセミコロンにマッチしているということです。そうでなければ、他の場合のように余計なセミコロンが出てくるはずです。
前述したように、セミコロン必須のアイテム文に関しては末尾のセミコロンがマッチ対象になるため、筋は通っています。
最後に、単独のブロック式や制御フロー式のみから構成される場合を除き、式が末尾のセミコロンつきで出力され直していることが観察できます。
ここで言及したことの詳細を知るには、リファレンスをあたってください。
幸い、これらの詳細は通常まったく重要ではありません。先述したように繰り返しに関する小さな例外はありますが、それだけではそこまで頻繁には問題にならないはずです。
デバッグを行う際のコツについてはデバッグの章を参照。
tt
tt
フラグメントは1つのトークン木にマッチします。
トークン木が正確には何なのかについて復習したければ、本書のトークン木の章を読み返すとよいでしょう。
tt
フラグメントは最も強力なフラグメントの一角です。ほぼすべてのものにマッチさせられるうえに、あとでその中身を調べる余地を残すこともできるためです。
このフラグメントは、tt-muncherやプッシュダウン累算器 (push-down-accumulator) といった非常に強力なパターンを可能にします。
ty
ty
フラグメントは、任意の種類の型の式 (type expression) にマッチします。
macro_rules! types { ($($type:ty)*) => (); } types! { foo::bar bool [u8] impl IntoIterator<Item = u32> } fn main() {}
vis
vis
フラグメントは空かもしれない可視性修飾子 (Visibility qualifier) にマッチします。
#![allow(unused)] fn main() { macro_rules! visibilities { // ∨~~Note this comma, since we cannot repeat a `vis` fragment on its own // v~~このコンマが必要なことに注意。`vis`フラグメント単体を繰り返すことはできないため。 ($($vis:vis,)*) => (); } visibilities! { , // no vis is fine, due to the implicit `?` // 暗黙の `?` により、可視性なしでも問題ない pub, pub(crate), pub(in super), pub(in some_path), } }
空のトークン列にマッチさせられるとはいえ、以下に説明するように、vis
フラグメントは選択的繰り返しとは大きく異なった動作をします。
vis
に空トークン列をマッチさせたあとに余るトークンがない場合、マクロのマッチング全体が失敗します。
#![allow(unused)] fn main() { macro_rules! non_optional_vis { ($vis:vis) => (); } non_optional_vis!(); // ^^^^^^^^^^^^^^^^ error: missing tokens in macro arguments }
$vis:vis $ident:ident
は問題なくマッチしますが、$(pub)? $ident:ident
は pub
が妥当な識別子であるために曖昧なのでマッチしません。
#![allow(unused)] fn main() { macro_rules! vis_ident { ($vis:vis $ident:ident) => (); } vis_ident!(pub foo); // this works fine // これは問題なく動く macro_rules! pub_ident { ($(pub)? $ident:ident) => (); } pub_ident!(pub foo); // ^^^ error: local ambiguity when calling macro `pub_ident`: multiple parsing options: built-in NTs ident ('ident') or 1 other option. }
空のトークン列にマッチするフラグメントは、tt
フラグメントや再帰的展開と組み合わせることでとても興味深い奇妙な動作を生み出します。
空のトークン列にマッチする場合であっても、vis
指定のメタ変数はやはりキャプチャとみなされ、さらに tt
, ident
, lifetime
フラグメントでないため以降の展開に対して不透明になります。
このキャプチャを別のマクロを呼び出す際に tt
として渡すと、実質的に何も含まないトークン木が手に入ることになります。
macro_rules! it_is_opaque { (()) => { "()" }; (($tt:tt)) => { concat!("$tt is ", stringify!($tt)) }; ($vis:vis ,) => { it_is_opaque!( ($vis) ); } } fn main() { // this prints "$tt is ", as the recursive calls hits the second branch with // an empty tt, opposed to matching with the first branch! // "$tt is " と表示される。再帰呼び出しが1番めの分岐にマッチしようとせず、 // 空のトークン木(tt)を伴って2番めの分岐に到達するため println!("{}", it_is_opaque!(,)); }
Metavariables and Expansion Redux
Once the parser begins consuming tokens for a metavariable, it cannot stop or backtrack. This means that the second rule of the following macro cannot ever match, no matter what input is provided:
macro_rules! dead_rule {
($e:expr) => { ... };
($i:ident +) => { ... };
}
Consider what happens if this macro is invoked as dead_rule!(x+)
.
The interpreter will start at the first rule, and attempt to parse the input as an expression.
The first token x
is valid as an expression.
The second token is also valid in an expression, forming a binary addition node.
At this point, given that there is no right-hand side of the addition, you might expect the parser to give up and try the next rule. Instead, the parser will panic and abort the entire compilation, citing a syntax error.
As such, it is important in general that you write macro rules from most-specific to least-specific.
To defend against future syntax changes altering the interpretation of macro input, macro_rules!
restricts what can follow various metavariables.
The complete list, showing what may follow what fragment specifier, as of Rust 1.46 is as follows:
stmt
andexpr
:=>
,,
, or;
pat
:=>
,,
,=
,if
,in
1pat_param
:=>
,,
,=
,|
,if
,in
path
andty
:=>
,,
,=
,|
,;
,:
,>
,>>
,[
,{
,as
,where
, or a macro variable of theblock
fragment specifier.vis
:,
, an identifier other than a non-rawpriv
, any token that can begin a type or a metavariable with anident
,ty
, orpath
fragment specifier.- All other fragment specifiers have no restrictions.
Edition Differences: Before the 2021 edition, pat
may also be followed by |
.
Repetitions also adhere to these restrictions, meaning if a repetition can repeat multiple times(*
or +
), then the contents must be able to follow themselves.
If a repetition can repeat zero times (?
or *
) then what comes after the repetition must be able to follow what comes before.
The parser also does not perform any kind of lookahead. That means if the compiler cannot unambiguously determine how to parse the macro invocation one token at a time, it will abort with an ambiguity error. A simple example that triggers this:
#![allow(unused)] fn main() { macro_rules! ambiguity { ($($i:ident)* $i2:ident) => { }; } // error: // local ambiguity: multiple parsing options: built-in NTs ident ('i') or ident ('i2'). ambiguity!(an_identifier); }
The parser does not look ahead past the identifier to see if the following token is a )
, which would allow it to parse properly.
One aspect of substitution that often surprises people is that substitution is not token-based, despite very much looking like it.
Consider the following:
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)); }
The output is:
got an identifier
got an addition
got something else
got something else
got something else
got something else
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.
Here is another example which can be particularly confusing:
macro_rules! capture_then_what_is { (#[$m:meta]) => {what_is!(#[$m])}; } macro_rules! what_is { (#[no_mangle]) => {"no_mangle attribute"}; (#[inline]) => {"inline attribute"}; ($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")}; } fn main() { println!( "{}\n{}\n{}\n{}", what_is!(#[no_mangle]), what_is!(#[inline]), capture_then_what_is!(#[no_mangle]), capture_then_what_is!(#[inline]), ); }
The output is:
no_mangle attribute
inline attribute
something else (#[no_mangle])
something else (#[inline])
The only way to avoid this is to capture using the tt
, ident
or lifetime
kinds.
Once you capture with anything else, the only thing you can do with the result from then on is substitute it directly into the output.
Metavariable Expressions
RFC: rfcs#1584
Tracking Issue: rust#83527
Feature:#![feature(macro_metavar_expr)]
Note: The example code snippets are very bare bones, trying to show off how they work. If you think you got small snippets with proper isolated usage of these expression please submit them!
As mentioned in the methodical introduction
, Rust has special expressions that can be used by macro transcribers to obtain information about metavariables that are otherwise difficult or even impossible to get.
This chapter will introduce them more in-depth together with usage examples.
Dollar Dollar ($$
)
The $$
expression expands to a single $
, making it effectively an escaped $
.
This enables the ability in writing macros emitting new macros as the former macro won't transcribe metavariables, repetitions and metavariable expressions that have an escaped $
.
We can see the problem without using $$
in the following snippet:
macro_rules! foo { () => { macro_rules! bar { ( $( $any:tt )* ) => { $( $any )* }; // ^^^^^^^^^^^ error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth } }; } foo!(); fn main() {}
The problem is obvious, the transcriber of foo sees a repetition and tries to repeat it when transcribing, but there is no $any
metavariable in its scope causing it to fail.
With $$
we can get around this as the transcriber of foo
will no longer try to do the repetition.1
#![feature(macro_metavar_expr)] macro_rules! foo { () => { macro_rules! bar { ( $$( $$any:tt )* ) => { $$( $$any )* }; } }; } foo!(); bar!(); fn main() {}
Before $$
occurs, users must resort to a tricky and not so well-known hack to declare nested macros with repetitions
via using $tt
like this.
count(ident, depth)
The count
metavariable expression expands to the repetition count of the metavariable $ident
up to the given repetition depth.
- The
ident
argument must be a declared metavariable in the scope of the rule. - The
depth
argument must be an integer literal of value less or equal to the maximum repetition depth that the$ident
metavariable appears in. - The expression expands to an unsuffixed integer literal token.
The count(ident)
expression defaults depth
to the maximum valid depth, making it count the total repetitions for the given metavariable.
#![feature(macro_metavar_expr)] macro_rules! foo { ( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) => { println!("count(outer, 0): $outer repeats {} times", ${count(outer)}); println!("count(inner, 0): The $inner repetition repeats {} times in the outer repetition", ${count(inner, 0)}); println!("count(inner, 1): $inner repeats {} times in the inner repetitions", ${count(inner, 1)}); }; } fn main() { foo! { outer () ; outer ( inner , inner ) ; outer () ; outer ( inner ) ; }; }
index(depth)
The index(depth)
metavariable expression expands to the current iteration index of the repetition at the given depth.
- The
depth
argument targets the repetition atdepth
counting outwards from the inner-most repetition where the expression is invoked. - The expression expands to an unsuffixed integer literal token.
The index()
expression defaults depth
to 0
, making it a shorthand for index(0)
.
#![feature(macro_metavar_expr)] macro_rules! attach_iteration_counts { ( $( ( $( $inner:ident ),* ) ; )* ) => { ( $( $(( stringify!($inner), ${index(1)}, // this targets the outer repetition ${index()} // and this, being an alias for `index(0)` targets the inner repetition ),)* )* ) }; } fn main() { let v = attach_iteration_counts! { ( hello ) ; ( indices , of ) ; () ; ( these, repetitions ) ; }; println!("{v:?}"); }
length(depth)
The length(depth)
metavariable expression expands to the iteration count of the repetition at the given depth.
- The
depth
argument targets the repetition atdepth
counting outwards from the inner-most repetition where the expression is invoked. - The expression expands to an unsuffixed integer literal token.
The length()
expression defaults depth
to 0
, making it a shorthand for length(0)
.
#![feature(macro_metavar_expr)] macro_rules! lets_count { ( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) => { $( $( println!( "'{}' in inner iteration {}/{} with '{}' in outer iteration {}/{} ", stringify!($inner), ${index()}, ${length()}, stringify!($outer), ${index(1)}, ${length(1)}, ); )* )* }; } fn main() { lets_count!( many (small , things) ; none () ; exactly ( one ) ; ); }
ignore(ident)
The ignore(ident)
metavariable expression expands to nothing, making it possible to expand something as often as a metavariable repeats without expanding the metavariable.
- The
ident
argument must be a declared metavariable in the scope of the rule.
#![feature(macro_metavar_expr)] macro_rules! repetition_tuples { ( $( ( $( $inner:ident ),* ) ; )* ) => { ($( $( ( ${index()}, ${index(1)} ${ignore(inner)} // without this metavariable expression, compilation would fail ), )* )*) }; } fn main() { let tuple = repetition_tuples!( ( one, two ) ; () ; ( one ) ; ( one, two, three ) ; ); println!("{tuple:?}"); }
Hygiene
macro_rules!
macros in Rust are partially hygienic, also called mixed hygiene.
Specifically, they are hygienic when it comes to local variables, labels and $crate
, but nothing else.
Hygiene works by attaching an invisible "syntax context" value to all identifiers. When two identifiers are compared, both the identifiers' textual names and syntax contexts must be identical for the two to be considered equal.
To illustrate this, consider the following code:
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);
We will use the background colour to denote the syntax context. Now, let's expand the macro invocation:
let four = {
let a = 42;
a / 10
};
First, recall that macro_rules!
invocations effectively disappear during expansion.
Second, if you attempt to compile this code, the compiler will respond with something along the following lines:
error[E0425]: cannot find value `a` in this scope
--> src/main.rs:13:21
|
13 | let four = using_a!(a / 10);
| ^ not found in this scope
Note that the background colour (i.e. syntax context) for the expanded macro changes as part of expansion.
Each macro_rules!
macro expansion is given a new, unique syntax context for its contents.
As a result, there are two different a
s in the expanded code: one in the first syntax context, the second in the other.
In other words, a
is not the same identifier as a
, however similar they may appear.
That said, tokens that were substituted into the expanded output retain their original syntax context (by virtue of having been provided to the macro as opposed to being part of the macro itself). Thus, the solution is to modify the macro as follows:
macro_rules! using_a {
($a:ident, $e:expr) => {
{
let $a = 42;
$e
}
}
}
let four = using_a!(a, a / 10);
Which, upon expansion becomes:
let four = {
let a = 42;
a / 10
};
The compiler will accept this code because there is only one a
being used.
$crate
Hygiene is also the reason that we need the $crate
metavariable when our macro needs access to other items in the defining crate.
What this special metavariable does is that it expands to an absolute path to the defining crate.
//// Definitions in the `helper_macro` crate.
#[macro_export]
macro_rules! helped {
// () => { helper!() } // This might lead to an error due to 'helper' not being in scope.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// Usage in another crate.
// Note that `helper_macro::helper` is not imported!
use helper_macro::helped;
fn unit() {
// but it still works due to `$crate` properly expanding to the crate path `helper_macro`
helped!();
}
Note that, because $crate
refers to the current crate, it must be used with a fully qualified module path when referring to non-macro items:
#![allow(unused)] fn main() { pub mod inner { #[macro_export] macro_rules! call_foo { () => { $crate::inner::foo() }; } pub fn foo() {} } }
Non-Identifier Identifiers
There are two tokens which you are likely to run into eventually that look like identifiers, but aren't. Except when they are.
First is self
.
This is very definitely a keyword.
However, it also happens to fit the definition of an identifier.
In regular Rust code, there's no way for self
to be interpreted as an identifier, but it can happen with macro_rules!
macros:
macro_rules! what_is { (self) => {"the keyword `self`"}; ($i:ident) => {concat!("the identifier `", stringify!($i), "`")}; } macro_rules! call_with_ident { ($c:ident($i:ident)) => {$c!($i)}; } fn main() { println!("{}", what_is!(self)); println!("{}", call_with_ident!(what_is(self))); }
The above outputs:
the keyword `self`
the keyword `self`
But that makes no sense; call_with_ident!
required an identifier, matched one, and substituted it!
So self
is both a keyword and not a keyword at the same time.
You might wonder how this is in any way important.
Take this example:
macro_rules! make_mutable { ($i:ident) => {let mut $i = $i;}; } struct Dummy(i32); impl Dummy { fn double(self) -> Dummy { make_mutable!(self); self.0 *= 2; self } } fn main() { println!("{:?}", Dummy(4).double().0); }
This fails to compile with:
error: `mut` must be followed by a named binding
--> src/main.rs:2:24
|
2 | ($i:ident) => {let mut $i = $i;};
| ^^^^^^ help: remove the `mut` prefix: `self`
...
9 | make_mutable!(self);
| -------------------- in this macro invocation
|
= note: `mut` may be followed by `variable` and `variable @ pattern`
So the macro will happily match self
as an identifier, allowing you to use it in cases where you can't actually use it.
But, fine; it somehow remembers that self
is a keyword even when it's an identifier, so you should be able to do this, right?
macro_rules! make_self_mutable { ($i:ident) => {let mut $i = self;}; } struct Dummy(i32); impl Dummy { fn double(self) -> Dummy { make_self_mutable!(mut_self); mut_self.0 *= 2; mut_self } } fn main() { println!("{:?}", Dummy(4).double().0); }
This fails with:
error[E0424]: expected value, found module `self`
--> src/main.rs:2:33
|
2 | ($i:ident) => {let mut $i = self;};
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter
...
8 | / fn double(self) -> Dummy {
9 | | make_self_mutable!(mut_self);
| | ----------------------------- in this macro invocation
10 | | mut_self.0 *= 2;
11 | | mut_self
12 | | }
| |_____- this function has a `self` parameter, but a macro invocation can only access identifiers it receives from parameters
|
Now the compiler thinks we refer to our module with self
, but that doesn't make sense.
We already have a self
right there, in the function signature which is definitely not a module.
It's almost like it's complaining that the self
it's trying to use isn't the same self
... as though the self
keyword has hygiene, like an... identifier.
macro_rules! double_method { ($body:expr) => { fn double(mut self) -> Dummy { $body } }; } struct Dummy(i32); impl Dummy { double_method! {{ self.0 *= 2; self }} } fn main() { println!("{:?}", Dummy(4).double().0); }
Same error. What about...
macro_rules! double_method { ($self_:ident, $body:expr) => { fn double(mut $self_) -> Dummy { $body } }; } struct Dummy(i32); impl Dummy { double_method! {self, { self.0 *= 2; self }} } fn main() { println!("{:?}", Dummy(4).double().0); }
At last, this works.
So self
is both a keyword and an identifier when it feels like it.
Surely this works for other, similar constructs, right?
macro_rules! double_method { ($self_:ident, $body:expr) => { fn double($self_) -> Dummy { $body } }; } struct Dummy(i32); impl Dummy { double_method! {_, 0} } fn main() { println!("{:?}", Dummy(4).double().0); }
error: no rules expected the token `_`
--> src/main.rs:12:21
|
1 | macro_rules! double_method {
| -------------------------- when calling this macro
...
12 | double_method! {_, 0}
| ^ no rules expected this token in macro call
No, of course not.
_
is a keyword that is valid in patterns and expressions, but somehow isn't an identifier like the keyword self
is, despite matching the definition of an identifier just the same.
You might think you can get around this by using $self_:pat
instead; that way, _
will match! Except, no, because self
isn't a pattern.
Joy.
The only work around for this (in cases where you want to accept some combination of these tokens) is to use a tt
matcher instead.
Debugging
Note: This is a list of debugging tools specifically tailored towards declarative macros, additional means of debugging these can be found in the debugging chapter of syntax extensions.
One of the most useful is trace_macros!
, which is a directive to the compiler instructing it to dump every macro_rules!
macro invocation prior to expansion.
For example, given the following:
// Note: make sure to use a nightly channel compiler.
#![feature(trace_macros)]
macro_rules! each_tt {
() => {};
($_tt:tt $($rest:tt)*) => {each_tt!($($rest)*);};
}
each_tt!(foo bar baz quux);
trace_macros!(true);
each_tt!(spim wak plee whum);
trace_macros!(false);
each_tt!(trom qlip winp xod);
fn main() {}
The output is:
note: trace_macro
--> src/main.rs:11:1
|
11 | each_tt!(spim wak plee whum);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expanding `each_tt! { spim wak plee whum }`
= note: to `each_tt ! (wak plee whum) ;`
= note: expanding `each_tt! { wak plee whum }`
= note: to `each_tt ! (plee whum) ;`
= note: expanding `each_tt! { plee whum }`
= note: to `each_tt ! (whum) ;`
= note: expanding `each_tt! { whum }`
= note: to `each_tt ! () ;`
= note: expanding `each_tt! { }`
= note: to ``
This is particularly invaluable when debugging deeply recursive macro_rules!
macros.
You can also enable this from the command-line by adding -Z trace-macros
to the compiler command line.
Secondly, there is log_syntax!
which causes the compiler to output all tokens passed to it.
For example, this makes the compiler sing a song:
// Note: make sure to use a nightly channel compiler. #![feature(log_syntax)] macro_rules! sing { () => {}; ($tt:tt $($rest:tt)*) => {log_syntax!($tt); sing!($($rest)*);}; } sing! { ^ < @ < . @ * '\x08' '{' '"' _ # ' ' - @ '$' && / _ % ! ( '\t' @ | = > ; '\x08' '\'' + '$' ? '\x7f' , # '"' ~ | ) '\x07' } fn main() {}
This can be used to do slightly more targeted debugging than trace_macros!
.
Another amazing tool is lukaslueg
's macro_railroad
, a tool that allows you visualize and generate syntax diagrams for Rust's macro_rules!
macros.
It visualizes the accepted macro's grammar as an automata.
Scoping
The way in which mbe macros are scoped can be somewhat unintuitive. They use two forms of scopes: textual scope, and path-based scope.
When such a macro is invoked by an unqualified identifier(an identifier that isn't part of a multi-segment path), it is first looked up in textual scoping and then in path-based scoping should the first lookup not yield any results. If it is invoked by a qualified identifier it will skip the textual scoping lookup and instead only do a look up in the path-based scoping.
Textual Scope
Firstly, unlike everything else in the language, function-like macros will remain visible in sub-modules.
macro_rules! X { () => {}; } mod a { X!(); // defined } mod b { X!(); // defined } mod c { X!(); // defined } fn main() {}
Note: In these examples, remember that all of them have the same behavior when the module contents are in separate files.
Secondly, also unlike everything else in the language, macro_rules!
macros are only accessible after their definition.
Also note that this example demonstrates how macro_rules!
macros do not "leak" out of their defining scope:
mod a { // X!(); // undefined } mod b { // X!(); // undefined macro_rules! X { () => {}; } X!(); // defined } mod c { // X!(); // undefined } fn main() {}
To be clear, this lexical order dependency applies even if you move the macro to an outer scope:
mod a { // X!(); // undefined } macro_rules! X { () => {}; } mod b { X!(); // defined } mod c { X!(); // defined } fn main() {}
However, this dependency does not apply to macros themselves:
mod a { // X!(); // undefined } macro_rules! X { () => { Y!(); }; } mod b { // X!(); // defined, but Y! is undefined } macro_rules! Y { () => {}; } mod c { X!(); // defined, and so is Y! } fn main() {}
Defining macro_rules!
macros multiple times is allowed and the most recent declaration will simply shadow previous ones unless it has gone out of scope.
#![allow(unused)] fn main() { macro_rules! X { (1) => {}; } X!(1); macro_rules! X { (2) => {}; } // X!(1); // Error: no rule matches `1` X!(2); mod a { macro_rules! X { (3) => {}; } // X!(2); // Error: no rule matches `2` X!(3); } // X!(3); // Error: no rule matches `3` X!(2); }
macro_rules!
macros can be exported from a module using the #[macro_use]
attribute.
Using this on a module is similar to saying that you do not want to have the module's macro's scope end with the module.
mod a { // X!(); // undefined } #[macro_use] mod b { macro_rules! X { () => {}; } X!(); // defined } mod c { X!(); // defined } fn main() {}
Note that this can interact in somewhat bizarre ways due to the fact that identifiers in a macro_rules!
macro (including other macros) are only resolved upon expansion:
mod a { // X!(); // undefined } #[macro_use] mod b { macro_rules! X { () => { Y!(); }; } // X!(); // defined, but Y! is undefined } macro_rules! Y { () => {}; } mod c { X!(); // defined, and so is Y! } fn main() {}
Another complication is that #[macro_use]
applied to an extern crate
does not behave this way: such declarations are effectively hoisted to the top of the module. Thus, assuming X!
is defined in an external crate called macs
, the following holds:
mod a {
// X!(); // defined, but Y! is undefined
}
macro_rules! Y { () => {}; }
mod b {
X!(); // defined, and so is Y!
}
#[macro_use] extern crate macs;
mod c {
X!(); // defined, and so is Y!
}
fn main() {}
Finally, note that these scoping behaviors apply to functions as well, with the exception of #[macro_use]
(which isn't applicable):
macro_rules! X { () => { Y!() }; } fn a() { macro_rules! Y { () => {"Hi!"} } assert_eq!(X!(), "Hi!"); { assert_eq!(X!(), "Hi!"); macro_rules! Y { () => {"Bye!"} } assert_eq!(X!(), "Bye!"); } assert_eq!(X!(), "Hi!"); } fn b() { macro_rules! Y { () => {"One more"} } assert_eq!(X!(), "One more"); } fn main() { a(); b(); }
These scoping rules are why a common piece of advice is to place all macro_rules!
macros which should be accessible "crate wide" at the very top of your root module, before any other modules.
This ensures they are available consistently.
This also applies to mod
definitions for files, as in:
#[macro_use]
mod some_mod_that_defines_macros;
mod some_mod_that_uses_those_macros;
The order here is important, swap the declaration order and it won't compile.
Path-Based Scope
By default, a macro_rules!
macro has no path-based scope.
However, if it has the #[macro_export]
attribute, then it is declared in the crate root scope and can be referred to similar to how you refer to any other item.
The Import and Export chapter goes more in-depth into said attribute.
Import and Export
Importing macro_rules!
macros differs between the two Rust Editions, 2015 and 2018.
It is recommended to read both parts nevertheless, as the 2018 Edition can still use the constructs that are explained in the 2015 Edition.
Edition 2015
In Edition 2015 you have to use the #[macro_use]
attribute that has already been introduced in the scoping chapter.
This can be applied to either modules or external crates.
For example:
#[macro_use] mod macros { macro_rules! X { () => { Y!(); } } macro_rules! Y { () => {} } } X!(); fn main() {}
macro_rules!
macros can be exported from the current crate using #[macro_export]
.
Note that this ignores all visibility.
Given the following definition for a library package macs
:
mod macros {
#[macro_export] macro_rules! X { () => { Y!(); } }
#[macro_export] macro_rules! Y { () => {} }
}
// X! and Y! are *not* defined here, but *are* exported,
// despite `macros` being private.
The following code will work as expected:
X!(); // X is defined
#[macro_use] extern crate macs;
X!();
fn main() {}
This works, as said in the scoping chapter, because #[macro_use]
works slightly different on extern crates, as it basically hoists the exported macros out of the crate to the top of the module.
Note: you can only
#[macro_use]
an external crate from the root module.
Finally, when importing macro_rules!
macros from an external crate, you can control which macros you import.
You can use this to limit namespace pollution, or to override specific macros, like so:
// Import *only* the `X!` macro.
#[macro_use(X)] extern crate macs;
// X!(); // X is defined, but Y! is undefined
macro_rules! Y { () => {} }
X!(); // X is defined, and so is Y!
fn main() {}
When exporting macro_rules!
macros, it is often useful to refer to non-macro symbols in the defining crate.
Because crates can be renamed, there is a special substitution variable available: $crate
.
This will always expand to an absolute path prefix to the containing crate (e.g. :: macs
).
Note that unless your compiler version is >= 1.30, this does not work for macro_rules!
macros, because macro_rules!
macros do not interact with regular name resolution in any way.
Otherwise, you cannot use something like $crate::Y!
to refer to a particular macro within your crate.
The implication, combined with selective imports via #[macro_use]
is that there is currently no way to guarantee any given macro will be available when imported by another crate.
It is recommended that you always use absolute paths to non-macro names, to avoid conflicts, including names in the standard library.
Edition 2018
The 2018 Edition made our lives a lot easier when it comes to macro_rules!
macros.
Why you ask?
Quite simply because it managed to make them feel more like proper items than some special thing in the language.
What this means is that we can properly import and use them in a namespaced fashion!
So instead of using #[macro_use]
to import every exported macro from a crate into the global namespace we can now do the following:
use some_crate::some_macro;
fn main() {
some_macro!("hello");
// as well as
some_crate::some_other_macro!("macro world");
}
Unfortunately, this only applies for external crates, if you use macro_rules!
macros that you have defined in your own crate you are still required to go with #[macro_use]
on the defining modules.
So scoping applies there the same way as before as well.
The
$crate
prefix works in this version for everything, macros and items alike since this Edition came out with Rust 1.31.
パターン
パースと展開のパターンをまとめます。
Callbacks
macro_rules! call_with_larch { ($callback:ident) => { $callback!(larch) }; } macro_rules! expand_to_larch { () => { larch }; } macro_rules! recognize_tree { (larch) => { println!("#1, the Larch.") }; (redwood) => { println!("#2, the Mighty Redwood.") }; (fir) => { println!("#3, the Fir.") }; (chestnut) => { println!("#4, the Horse Chestnut.") }; (pine) => { println!("#5, the Scots Pine.") }; ($($other:tt)*) => { println!("I don't know; some kind of birch maybe?") }; } fn main() { recognize_tree!(expand_to_larch!()); call_with_larch!(recognize_tree); }
Due to the order that macros are expanded in, it is (as of Rust 1.2) impossible to pass information to a macro from the expansion of another macro. This can make modularizing macros very difficult.
An alternative is to use recursion and pass a callback. Here is a trace of the above example to demonstrate how this takes place:
recognize_tree! { expand_to_larch ! ( ) }
println! { "I don't know; some kind of birch maybe?" }
// ...
call_with_larch! { recognize_tree }
recognize_tree! { larch }
println! { "#1, the Larch." }
// ...
Using a tt
repetition, one can also forward arbitrary arguments to a callback.
macro_rules! callback { ($callback:ident( $($args:tt)* )) => { $callback!( $($args)* ) }; } fn main() { callback!(callback(println("Yes, this *was* unnecessary."))); }
You can, of course, insert additional tokens in the arguments as needed.
Incremental TT Munchers
macro_rules! mixed_rules { () => {}; (trace $name:ident; $($tail:tt)*) => { { println!(concat!(stringify!($name), " = {:?}"), $name); mixed_rules!($($tail)*); } }; (trace $name:ident = $init:expr; $($tail:tt)*) => { { let $name = $init; println!(concat!(stringify!($name), " = {:?}"), $name); mixed_rules!($($tail)*); } }; } fn main() { let a = 42; let b = "Ho-dee-oh-di-oh-di-oh!"; let c = (false, 2, 'c'); mixed_rules!( trace a; trace b; trace c; trace b = "They took her where they put the crazies."; trace b; ); }
This pattern is perhaps the most powerful macro parsing technique available, allowing one to parse grammars of significant complexity. However, it can increase compile times if used excessively, so should be used with care.
A TT muncher is a recursive macro_rules!
macro that works by incrementally processing its input one step at a time.
At each step, it matches and removes (munches) some sequence of tokens from the start of its input, generates some intermediate output, then recurses on the input tail.
The reason for "TT" in the name specifically is that the unprocessed part of the input is always captured as $($tail:tt)*
.
This is done as a tt
repetition is the only way to losslessly capture part of a macro's input.
The only hard restrictions on TT munchers are those imposed on the macro_rules!
macro system as a whole:
- You can only match against literals and grammar constructs which can be captured by
macro_rules!
. - You cannot match unbalanced groups.
It is important, however, to keep the macro recursion limit in mind.
macro_rules!
does not have any form of tail recursion elimination or optimization.
It is recommended that, when writing a TT muncher, you make reasonable efforts to keep recursion as limited as possible.
This can be done by adding additional rules to account for variation in the input (as opposed to recursion into an intermediate layer), or by making compromises on the input syntax to make using standard repetitions more tractable.
Performance
TT munchers are inherently quadratic. Consider a TT muncher rule that consumes one token tree and then recursively calls itself on the remaining input. If it is passed 100 token trees:
- The initial invocation will match against all 100 token trees.
- The first recursive invocation will match against 99 token trees.
- The next recursive invocation will match against 98 token trees.
And so on, down to 1. This is a classic quadratic pattern, and long inputs can cause macro expansion to blow out compile times.
Try to avoid using TT munchers too much, especially with long inputs.
The default value of the recursion_limit
attribute is a good sanity check; if you have to exceed it, you might be heading for trouble.
If you have the choice between writing a TT muncher that can be called once to handle multiple things, or a simpler macro that can be called multiple times to handle a single thing, prefer the latter. For example, you could change a macro that is called like this:
#![allow(unused)] fn main() { macro_rules! f { ($($tt:tt)*) => {} } f! { fn f_u8(x: u32) -> u8; fn f_u16(x: u32) -> u16; fn f_u32(x: u32) -> u32; fn f_u64(x: u64) -> u64; fn f_u128(x: u128) -> u128; } }
To one that is called like this:
#![allow(unused)] fn main() { macro_rules! f { ($($tt:tt)*) => {} } f! { fn f_u8(x: u32) -> u8; } f! { fn f_u16(x: u32) -> u16; } f! { fn f_u32(x: u32) -> u32; } f! { fn f_u64(x: u64) -> u64; } f! { fn f_u128(x: u128) -> u128; } }
The longer the input, the more likely this will improve compile times.
Also, if a TT muncher macro has many rules, put the most frequently matched rules as early as possible. This avoids unnecessary matching failures. (In fact, this is good advice for any kind of declarative macro, not just TT munchers.)
Finally, if you can write a macro using normal repetition via *
or +
, that should be preferred to a TT muncher.
This is most likely if each invocation of the TT muncher would only process one token at a time.
In more complicated cases, there is an advanced technique used within the quote
crate that can avoid the quadratic behaviour, at the cost of some conceptual complexity.
See this comment for details.
Internal Rules
#[macro_export] macro_rules! foo { (@as_expr $e:expr) => {$e}; ($($tts:tt)*) => { foo!(@as_expr $($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
Internal rules can be used to unify multiple macro_rules!
macros into one, or to make it easier to read and write TT Munchers by explicitly naming what rule you wish to call in a macro.
So why is it useful to unify multiple macros-by-example into one?
The main reasoning for this is how they are handled in the 2015 Edition of Rust due to macro_rules!
macros not being namespaced in said edition.
This gives one the troubles of having to re-export all the internal macro_rules!
macros as well as polluting the global macro namespace or even worse, macro name collisions with other crates.
In short, it's quite a hassle.
This fortunately isn't really a problem anymore nowadays with a rustc version >= 1.30, for more information consult the Import and Export chapter.
Nevertheless, let's talk about how we can unify multiple macro_rules!
macros into one with this technique and what exactly this technique even is.
We have two macro_rules!
macros, the common as_expr!
macro and a foo
macro that makes use of the first one:
#[macro_export] macro_rules! as_expr { ($e:expr) => {$e} } #[macro_export] macro_rules! foo { ($($tts:tt)*) => { as_expr!($($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
This is definitely not the nicest solution we could have for this macro, as it pollutes the global macro namespace as mentioned earlier.
In this specific case as_expr
is also a very simple macro that we only used once, so let's "embed" this macro in our foo
macro with internal rules!
To do so, we simply prepend a new matcher for our macro, which consists of the matcher used in the as_expr
macro, but with a small addition.
We prepend a tokentree that makes it match only when specifically asked to.
In this case we can for example use @as_expr
, so our matcher becomes (@as_expr $e:expr) => {$e};
.
With this we get the macro that was defined at the very top of this page:
#[macro_export] macro_rules! foo { (@as_expr $e:expr) => {$e}; ($($tts:tt)*) => { foo!(@as_expr $($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
You see how we embedded the as_expr
macro in the foo
one?
All that changed is that instead of invoking the as_expr
macro, we now invoke foo
recursively but with a special token tree prepended to the arguments, foo!(@as_expr $($tts)*)
.
If you look closely you might even see that this pattern can be combined quite nicely with TT Munchers!
The reason for using @
was that, as of Rust 1.2, the @
token is not used in prefix position; as such, it cannot conflict with anything.
This reasoning became obsolete later on when in Rust 1.7 macro matchers got future proofed by emitting a warning to prevent certain tokens from being allowed to follow certain fragments1, which in Rust 1.12 became a hard-error.
Other symbols or unique prefixes may be used as desired, but use of @
has started to become widespread, so using it may aid readers in understanding your macro.
Note: in the early days of Rust the
@
token was previously used in prefix position to denote a garbage-collected pointer, back when the language used sigils to denote pointer types. Its only current purpose is for binding names to patterns. For this, however, it is used as an infix operator, and thus does not conflict with its use here.
Additionally, internal rules will often come before any "bare" rules, to avoid issues with macro_rules!
incorrectly attempting to parse an internal invocation as something it cannot possibly be, such as an expression.
Performance
One downside of internal rules is that they can hurt compile times. Only one macro rule can match any (valid) macro invocation, but the compiler must try to match all rules in order. If a macro has many rules, there can be many such failures, and the use of internal rules will increase the number of such failures.
Also, the @as_expr
-style identifier makes rules longer, slightly increasing
the amount of work the compiler must do when matching.
Therefore, for best performance, avoiding internal rules is best. Avoiding them often makes complex macros easier to read, too.
Push-down Accumulation
The following macro uses push-down accumulation.
#![allow(unused)] fn main() { macro_rules! init_array { [$e:expr; $n:tt] => { { let e = $e; accum!([$n, e.clone()] -> []) } }; } macro_rules! accum { ([3, $e:expr] -> [$($body:tt)*]) => { accum!([2, $e] -> [$($body)* $e,]) }; ([2, $e:expr] -> [$($body:tt)*]) => { accum!([1, $e] -> [$($body)* $e,]) }; ([1, $e:expr] -> [$($body:tt)*]) => { accum!([0, $e] -> [$($body)* $e,]) }; ([0, $_:expr] -> [$($body:tt)*]) => { [$($body)*] }; } let strings: [String; 3] = init_array![String::from("hi!"); 3]; assert_eq!(format!("{:?}", strings), "[\"hi!\", \"hi!\", \"hi!\"]"); }
All syntax extensions in Rust must result in a complete, supported syntax element (such as an expression, item, etc.). This means that it is impossible to have a syntax extension expand to a partial construct.
One might hope that the above example could be more directly expressed like so:
macro_rules! init_array {
[$e:expr; $n:tt] => {
{
let e = $e;
[accum!($n, e.clone())]
}
};
}
macro_rules! accum {
(3, $e:expr) => { $e, accum!(2, $e) };
(2, $e:expr) => { $e, accum!(1, $e) };
(1, $e:expr) => { $e };
}
The expectation is that the expansion of the array literal would proceed as follows:
[accum!(3, e.clone())]
[e.clone(), accum!(2, e.clone())]
[e.clone(), e.clone(), accum!(1, e.clone())]
[e.clone(), e.clone(), e.clone()]
However, this would require each intermediate step to expand to an incomplete expression. Even though the intermediate results will never be used outside of a macro context, it is still forbidden.
Push-down, however, allows us to incrementally build up a sequence of tokens without needing to actually have a complete construct at any point prior to completion. In the example given at the top, the sequence of invocations proceeds as follows:
init_array!(String::from("hi!"); 3)
accum!([3, e.clone()] -> [])
accum!([2, e.clone()] -> [e.clone(),])
accum!([1, e.clone()] -> [e.clone(), e.clone(),])
accum!([0, e.clone()] -> [e.clone(), e.clone(), e.clone(),])
[e.clone(), e.clone(), e.clone(),]
As you can see, each layer adds to the accumulated output until the terminating rule finally emits it as a complete construct.
The only critical part of the above formulation is the use of $($body:tt)*
to preserve the output without triggering parsing.
The use of ($input) -> ($output)
is simply a convention adopted to help clarify the behavior of such macros.
Push-down accumulation is frequently used as part of incremental TT munchers, as it allows arbitrarily complex intermediate results to be constructed. Internal Rules were of use here as well, as they simplify creating such macros.
Performance
Push-down accumulation is inherently quadratic. Consider a push-down accumulation rule that builds up an accumulator of 100 token trees, one token tree per invocation.
- The initial invocation will match against the empty accumulator.
- The first recursive invocation will match against the accumulator of 1 token tree.
- The next recursive invocation will match against the accumulator of 2 token trees.
And so on, up to 100. This is a classic quadratic pattern, and long inputs can cause macro expansion to blow out compile times. Furthermore, TT munchers are also inherently quadratic over their input, so a macro that uses both TT munching and push-down accumulation will be doubly quadratic!
All the performance advice about TT munchers holds for push-down accumulation. In general, avoid using them too much, and keep them as simple as possible.
Finally, make sure you put the accumulator at the end of rules, rather than the beginning. That way, if a rule fails, the compiler won't have had to match the (potentially long) accumulator before hitting the part of the rule that fails to match. This can make a large difference to compile times.
Repetition Replacement
macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}
This pattern is where a matched repetition sequence is simply discarded, with the variable being used to instead drive some repeated pattern that is related to the input only in terms of length.
For example, consider constructing a default instance of a tuple with more than 12 elements (the limit as of Rust 1.2).
#![allow(unused)] fn main() { macro_rules! tuple_default { ($($tup_tys:ty),*) => { ( $( replace_expr!( ($tup_tys) Default::default() ), )* ) }; } macro_rules! replace_expr { ($_t:tt $sub:expr) => {$sub}; } assert_eq!(tuple_default!(i32, bool, String), (i32::default(), bool::default(), String::default())); }
JFTE: we could have simply used
$tup_tys::default()
.
Here, we are not actually using the matched types. Instead, we throw them away and replace them with a single, repeated expression. To put it another way, we don't care what the types are, only how many there are.
TT Bundling
macro_rules! call_a_or_b_on_tail { ((a: $a:ident, b: $b:ident), call a: $($tail:tt)*) => { $a(stringify!($($tail)*)) }; ((a: $a:ident, b: $b:ident), call b: $($tail:tt)*) => { $b(stringify!($($tail)*)) }; ($ab:tt, $_skip:tt $($tail:tt)*) => { call_a_or_b_on_tail!($ab, $($tail)*) }; } fn compute_len(s: &str) -> Option<usize> { Some(s.len()) } fn show_tail(s: &str) -> Option<usize> { println!("tail: {:?}", s); None } fn main() { assert_eq!( call_a_or_b_on_tail!( (a: compute_len, b: show_tail), the recursive part that skips over all these tokens does not much care whether we will call a or call b: only the terminal rules care. ), None ); assert_eq!( call_a_or_b_on_tail!( (a: compute_len, b: show_tail), and now, to justify the existence of two paths we will also call a: its input should somehow be self-referential, so let us make it return some ninety-one! ), Some(91) ); }
In particularly complex recursive macros, a large number of arguments may be needed in order to carry identifiers and expressions to successive layers. However, depending on the implementation there may be many intermediate layers which need to forward these arguments, but do not need to use them.
As such, it can be very useful to bundle all such arguments together into a single TT by placing them in a group.
This allows layers which do not need to use the arguments to simply capture and substitute a single tt
, rather than having to exactly capture and substitute the entire argument group.
The example above bundles the $a
and $b
expressions into a group which can then be forwarded as a single tt
by the recursive rule.
This group is then destructured by the terminal rules to access the expressions.
構成要素 (Building Blocks)
macro_rules!
マクロのコードにおいて再利用できるスニペットをまとめます。
AST Coercion
The Rust parser is not very robust in the face of tt
substitutions.
Problems can arise when the parser is expecting a particular grammar construct and instead finds a lump of substituted tt
tokens.
Rather than attempt to parse them, it will often just give up. In these cases, it is necessary to employ an AST coercion.
#![allow(dead_code)] macro_rules! as_expr { ($e:expr) => {$e} } macro_rules! as_item { ($i:item) => {$i} } macro_rules! as_pat { ($p:pat) => {$p} } macro_rules! as_stmt { ($s:stmt) => {$s} } macro_rules! as_ty { ($t:ty) => {$t} } as_item!{struct Dummy;} fn main() { as_stmt!(let as_pat!(_): as_ty!(_) = as_expr!(42)); }
These coercions are often used with push-down accumulation macros in order to get the parser to treat the final tt
sequence as a particular kind of grammar construct.
Note that this specific set of macros is determined by what macros are allowed to expand to, not what they are able to capture.
Counting
What follows are several techniques for counting in macro_rules!
macros:
Note: If you are just interested in the most efficient way look here
Repetition with replacement
Counting things in a macro is a surprisingly tricky task. The simplest way is to use replacement with a repetition match.
macro_rules! replace_expr { ($_t:tt $sub:expr) => {$sub}; } macro_rules! count_tts { ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*}; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
This is a fine approach for smallish numbers, but will likely crash the compiler with inputs of around 500 or so tokens. Consider that the output will look something like this:
0usize + 1usize + /* ~500 `+ 1usize`s */ + 1usize
The compiler must parse this into an AST, which will produce what is effectively a perfectly unbalanced binary tree 500+ levels deep.
Recursion
An older approach is to use recursion.
macro_rules! count_tts { () => {0usize}; ($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)}; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
Note: As of
rustc
1.2, the compiler has grievous performance problems when large numbers of integer literals of unknown type must undergo inference. We are using explicitlyusize
-typed literals here to avoid that.If this is not suitable (such as when the type must be substitutable), you can help matters by using
as
(e.g.0 as $ty
,1 as $ty
, etc.).
This works, but will trivially exceed the recursion limit. Unlike the repetition approach, you can extend the input size by matching multiple tokens at once.
macro_rules! count_tts { ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt $_k:tt $_l:tt $_m:tt $_n:tt $_o:tt $_p:tt $_q:tt $_r:tt $_s:tt $_t:tt $($tail:tt)*) => {20usize + count_tts!($($tail)*)}; ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt $($tail:tt)*) => {10usize + count_tts!($($tail)*)}; ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt $($tail:tt)*) => {5usize + count_tts!($($tail)*)}; ($_a:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)}; () => {0usize}; } fn main() { assert_eq!(700, count_tts!( ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, // Repetition breaks somewhere after this ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, )); }
This particular formulation will work up to ~1,200 tokens.
Slice length
A third approach is to help the compiler construct a shallow AST that won't lead to a stack overflow.
This can be done by constructing an array literal and calling the len
method.
macro_rules! replace_expr { ($_t:tt $sub:expr) => {$sub}; } macro_rules! count_tts { ($($tts:tt)*) => {<[()]>::len(&[$(replace_expr!($tts ())),*])}; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
This has been tested to work up to 10,000 tokens, and can probably go much higher.
Array length
Another modification of the previous approach is to use const generics stabilized in Rust 1.51. It's only slightly slower than slice length method on 20,000 tokens and works in const contexts.
const fn count_helper<const N: usize>(_: [(); N]) -> usize { N } macro_rules! replace_expr { ($_t:tt $sub:expr) => { $sub } } macro_rules! count_tts { ($($smth:tt)*) => { count_helper([$(replace_expr!($smth ())),*]) } } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
Enum counting
This approach can be used where you need to count a set of mutually distinct identifiers.
macro_rules! count_idents { () => {0}; ($last_ident:ident, $($idents:ident),* $(,)?) => { { #[allow(dead_code, non_camel_case_types)] enum Idents { $($idents,)* $last_ident } const COUNT: u32 = Idents::$last_ident as u32 + 1; COUNT } }; } fn main() { const COUNT: u32 = count_idents!(A, B, C); assert_eq!(COUNT, 3); }
This method does have two drawbacks. As implied above, it can only count valid identifiers (which are also not keywords), and it does not allow those identifiers to repeat.
Bit twiddling
Another recursive approach using bit operations:
macro_rules! count_tts { () => { 0 }; ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }; ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
This approach is pretty smart as it effectively halves its input whenever its even and then multiplying the counter by 2 (or in this case shifting 1 bit to the left which is equivalent).
If the input is uneven it simply takes one token tree from the input or
s the token tree to the previous counter which is equivalent to adding 1 as the lowest bit has to be a 0 at this point due to the previous shifting.
Rinse and repeat until we hit the base rule () => 0
.
The benefit of this is that the constructed AST expression that makes up the counter value will grow with a complexity of O(log(n))
instead of O(n)
like the other approaches.
Be aware that you can still hit the recursion limit with this if you try hard enough.
Credits for this method go to Reddit user YatoRust
.
Let's go through the procedure by hand once:
count_tts!(0 0 0 0 0 0 0 0 0 0);
This invocation will match the third rule due to the fact that we have an even number of token trees(10).
The matcher names the odd token trees in the sequence $a
and the even ones $even
but the expansion only makes use of $a
, which means it effectively discards all the even elements cutting the input in half.
So the invocation now becomes:
count_tts!(0 0 0 0 0) << 1;
This invocation will now match the second rule as its input is an uneven amount of token trees. In this case the first token tree is discarded to make the input even again, then we also do the halving step in this invocation again since we know the input would be even now anyways. Therefor we can count 1 for the uneven discard and multiply by 2 again since we also halved.
((count_tts!(0 0) << 1) | 1) << 1;
((count_tts!(0) << 1 << 1) | 1) << 1;
(((count_tts!() | 1) << 1 << 1) | 1) << 1;
((((0 << 1) | 1) << 1 << 1) | 1) << 1;
Now to check if we expanded correctly manually we can use a one of the tools we introduced for debugging
.
When expanding the macro there we should get:
((((0 << 1) | 1) << 1 << 1) | 1) << 1;
That's the same so we didn't make any mistakes, great!
Abacus Counters
Provisional: needs a more compelling example. Matching nested groups that are not denoted by Rust groups is sufficiently unusual that it may not merit inclusion.
Note: this section assumes understanding of push-down accumulation and incremental TT munchers.
macro_rules! abacus { ((- $($moves:tt)*) -> (+ $($count:tt)*)) => { abacus!(($($moves)*) -> ($($count)*)) }; ((- $($moves:tt)*) -> ($($count:tt)*)) => { abacus!(($($moves)*) -> (- $($count)*)) }; ((+ $($moves:tt)*) -> (- $($count:tt)*)) => { abacus!(($($moves)*) -> ($($count)*)) }; ((+ $($moves:tt)*) -> ($($count:tt)*)) => { abacus!(($($moves)*) -> (+ $($count)*)) }; // Check if the final result is zero. (() -> ()) => { true }; (() -> ($($count:tt)+)) => { false }; } fn main() { let equals_zero = abacus!((++-+-+++--++---++----+) -> ()); assert_eq!(equals_zero, true); }
This technique can be used in cases where you need to keep track of a varying counter that starts at or near zero, and must support the following operations:
- Increment by one.
- Decrement by one.
- Compare to zero (or any other fixed, finite value).
A value of n is represented by n instances of a specific token stored in a group.
Modifications are done using recursion and push-down accumulation.
Assuming the token used is x
, the operations above are implemented as follows:
- Increment by one: match
($($count:tt)*)
, substitute(x $($count)*)
. - Decrement by one: match
(x $($count:tt)*)
, substitute($($count)*)
. - Compare to zero: match
()
. - Compare to one: match
(x)
. - Compare to two: match
(x x)
. - (and so on...)
In this way, operations on the counter are like flicking tokens back and forth like an abacus.1
This desperately thin reasoning conceals the real reason for this name: to avoid having
yet another thing with "token" in the name. Talk to your writer about avoiding
semantic satiation today!
In fairness, it could also have been called "unary counting".
In cases where you want to represent negative values, -n can be represented as n instances of a
different token.
In the example given above, +n is stored as n +
tokens, and -m is stored as m -
tokens.
In this case, the operations become slightly more complicated; increment and decrement effectively reverse their usual meanings when the counter is negative.
To which given +
and -
for the positive and negative tokens respectively, the operations change to:
- Increment by one:
- match
()
, substitute(+)
. - match
(- $($count:tt)*)
, substitute($($count)*)
. - match
($($count:tt)+)
, substitute(+ $($count)+)
.
- match
- Decrement by one:
- match
()
, substitute(-)
. - match
(+ $($count:tt)*)
, substitute($($count)*)
. - match
($($count:tt)+)
, substitute(- $($count)+)
.
- match
- Compare to 0: match
()
. - Compare to +1: match
(+)
. - Compare to -1: match
(-)
. - Compare to +2: match
(++)
. - Compare to -2: match
(--)
. - (and so on...)
Note that the example at the top combines some of the rules together (for example, it combines increment on ()
and ($($count:tt)+)
into an increment on ($($count:tt)*)
).
If you want to extract the actual value of the counter, this can be done using a regular counter macro. For the example above, the terminal rules can be replaced with the following:
macro_rules! abacus {
// ...
// This extracts the counter as an integer expression.
(() -> ()) => {0};
(() -> (- $($count:tt)*)) => {
- ( count_tts!($( $count_tts:tt )*) )
};
(() -> (+ $($count:tt)*)) => {
count_tts!($( $count_tts:tt )*)
};
}
// One of the many token tree counting macros in the counting chapter
macro_rules! count_tts {
// ...
}
JFTE: strictly speaking, the above formulation of
abacus!
is needlessly complex. It can be implemented much more efficiently using repetition, provided you do not need to match against the counter's value in a macro:macro_rules! abacus { (-) => {-1}; (+) => {1}; ($( $moves:tt )*) => { 0 $(+ abacus!($moves))* } }
Parsing Rust
Parsing some of Rust's items can be useful in certain situations. This section will show a few macros that can parse some of Rust's more complex items like structs and functions to a certain extent. The goal of these macros is not to be able to parse the entire grammar of the items but to parse parts that are in general quite useful without being too complex to parse. This means we ignore things like generics and such.
The main points of interest of these macros are their matchers
.
The transcribers are only there for example purposes and are usually not that impressive.
Function
macro_rules! function_item_matcher { ( $( #[$meta:meta] )* // ^~~~attributes~~~~^ $vis:vis fn $name:ident ( $( $arg_name:ident : $arg_ty:ty ),* $(,)? ) // ^~~~~~~~~~~~~~~~argument list!~~~~~~~~~~~~~~^ $( -> $ret_ty:ty )? // ^~~~return type~~~^ { $($tt:tt)* } // ^~~~~body~~~~^ ) => { $( #[$meta] )* $vis fn $name ( $( $arg_name : $arg_ty ),* ) $( -> $ret_ty )? { $($tt)* } } } function_item_matcher!( #[inline] #[cold] pub fn foo(bar: i32, baz: i32, ) -> String { format!("{} {}", bar, baz) } ); fn main() { assert_eq!(foo(13, 37), "13 37"); }
A simple function matcher that ignores qualifiers like unsafe
, async
, ... as well as generics and where clauses.
If parsing those is required it is likely that you are better off using a proc-macro instead.
This lets you for example, inspect the function signature, generate some extra things from it and then re-emit the entire function again.
Kind of like a Derive
proc-macro but weaker and for functions.
Ideally we would like to use a pattern fragment specifier instead of an ident for the arguments but this is currently not allowed. Fortunately people don't use non-identifier patterns in function signatures that often so this is okay(a shame, really).
Method
The macro for parsing basic functions is nice and all, but sometimes we would like to also parse methods, functions that refer to their object via some form of self
usage. This makes things a bit trickier:
WIP
Struct
macro_rules! struct_item_matcher { // Unit-Struct ( $( #[$meta:meta] )* // ^~~~attributes~~~~^ $vis:vis struct $name:ident; ) => { $( #[$meta] )* $vis struct $name; }; // Tuple-Struct ( $( #[$meta:meta] )* // ^~~~attributes~~~~^ $vis:vis struct $name:ident ( $( $( #[$field_meta:meta] )* // ^~~~field attributes~~~~^ $field_vis:vis $field_ty:ty // ^~~~~~a single field~~~~~~^ ),* $(,)? ); ) => { $( #[$meta] )* $vis struct $name ( $( $( #[$field_meta] )* $field_vis $field_ty ),* ); }; // Named-Struct ( $( #[$meta:meta] )* // ^~~~attributes~~~~^ $vis:vis struct $name:ident { $( $( #[$field_meta:meta] )* // ^~~~field attributes~~~!^ $field_vis:vis $field_name:ident : $field_ty:ty // ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^ ),* $(,)? } ) => { $( #[$meta] )* $vis struct $name { $( $( #[$field_meta] )* $field_vis $field_name : $field_ty ),* } } } struct_item_matcher!( #[derive(Copy, Clone)] pub(crate) struct Foo { pub bar: i32, baz: &'static str, qux: f32 } ); struct_item_matcher!( #[derive(Copy, Clone)] pub(crate) struct Bar; ); struct_item_matcher!( #[derive(Clone)] pub(crate) struct Baz (i32, pub f32, String); ); fn main() { let _: Foo = Foo { bar: 42, baz: "macros can be nice", qux: 3.14, }; let _: Bar = Bar; let _: Baz = Baz(2, 0.1234, String::new()); }
Enum
Parsing enums is a bit more complex than structs so we will finally make use of some of the patterns we have discussed, Incremental TT Muncher and Internal Rules. Instead of just building the parsed enum again we will merely visit all the tokens of the enum, as rebuilding the enum would require us to collect all the parsed tokens temporarily again via a Push Down Accumulator.
#![allow(unused)] fn main() { macro_rules! enum_item_matcher { // tuple variant (@variant $variant:ident ( $( $( #[$field_meta:meta] )* // ^~~~field attributes~~~~^ $field_vis:vis $field_ty:ty // ^~~~~~a single field~~~~~~^ ),* $(,)? //∨~~rest of input~~∨ ) $(, $($tt:tt)* )? ) => { // process rest of the enum $( enum_item_matcher!(@variant $( $tt )*) )? }; // named variant (@variant $variant:ident { $( $( #[$field_meta:meta] )* // ^~~~field attributes~~~!^ $field_vis:vis $field_name:ident : $field_ty:ty // ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^ ),* $(,)? //∨~~rest of input~~∨ } $(, $($tt:tt)* )? ) => { // process rest of the enum $( enum_item_matcher!(@variant $( $tt )*) )? }; // unit variant (@variant $variant:ident $(, $($tt:tt)* )? ) => { // process rest of the enum $( enum_item_matcher!(@variant $( $tt )*) )? }; // trailing comma (@variant ,) => {}; // base case (@variant) => {}; // entry point ( $( #[$meta:meta] )* $vis:vis enum $name:ident { $($tt:tt)* } ) => { enum_item_matcher!(@variant $($tt)*) }; } enum_item_matcher!( #[derive(Copy, Clone)] pub(crate) enum Foo { Bar, Baz, } ); enum_item_matcher!( #[derive(Copy, Clone)] pub(crate) enum Bar { Foo(i32, f32), Bar, Baz(), } ); enum_item_matcher!( #[derive(Clone)] pub(crate) enum Baz {} ); }
マクロ2.0
RFC: rfcs#1584
トラッキングIssue: rust#39412
機能フラグ:#![feature(decl_macro)]
While not yet stable(or rather far from being finished), there is proposal for a new declarative macro system that is supposed to replace macro_rules!
dubbed declarative macros 2.0, macro
, decl_macro
or confusingly also macros-by-example
.
This chapter is only meant to quickly glance over the current state, showing how to use this macro system and where it differs. Nothing described here is final or complete, and may be subject to change.
Syntax
We'll do a comparison between the macro
and macro_rules
syntax for two macros we have implemented in previous chapters:
#![allow(unused)] #![feature(decl_macro)] fn main() { macro_rules! replace_expr_ { ($_t:tt $sub:expr) => { $sub } } macro replace_expr($_t:tt $sub:expr) { $sub } macro_rules! count_tts_ { () => { 0 }; ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }; ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }; } macro count_tts { () => { 0 }, ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }, ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }, } }
As can be seen, they look very similar, with just a few differences as well as that macro
s have two different forms.
Let's inspect the count_tts
macro first, as that one looks more like what we are used to.
As can be seen, it practically looks identical to the macro_rules
version with two exceptions, it uses the macro
keyword and the rule separator is a ,
instead of a ;
.
There is a second form to this though, which is a shorthand for macros that only have one rule.
Taking a look at replace_expr
we can see that in this case we can write the definition in a way that more resembles an ordinary function.
We can write the matcher directly after the name followed by the transcriber, dropping a pair of braces and the =>
token.
Syntax for invoking macro
s is the same as for macro_rules
and function-like procedural macros, the name followed by a !
followed by the macro input token tree.
macro
are proper items
Unlike with macro_rules
macros, which are textually scoped and require #[macro_export]
(and potentially a re-export) to be treated as an item, macro
macros behave like proper rust items by default.
As such, you can properly qualify them with visibility specifiers like pub
, pub(crate)
, pub(in path)
and the like.
Hygiene
Hygiene is by far the biggest difference between the two declarative macro systems.
Unlike macro_rules
which have mixed site hygiene, macro
have definition site hygiene, meaning they do not leak identifiers outside of their invocation.
As such the following compiles with a macro_rules
macro, but fails with a macro
definition:
#![feature(decl_macro)] // try uncommenting the following line, and commenting out the line right after macro_rules! foo { // macro foo { ($name: ident) => { pub struct $name; impl $name { pub fn new() -> $name { $name } } } } foo!(Foo); fn main() { // this fails with a `macro`, but succeeds with a `macro_rules` let foo = Foo::new(); }
There may be plans to allow escaping hygiene for identifiers(hygiene bending) in the future.
Procedural Macros
Note: This section is still very incomplete!
This chapter will introduce Rust's second syntax extension type, procedural macros.
As with the declarative macros chapter, this one is also split into a methodical and a (WIP) practical subchapter with the former being a more formal introduction and the latter being a more practical oriented one.
A lot of the basic information covered has been sourced from the rust reference, as most knowledge about procedural macros is currently located there.
A Methodical Introduction
This chapter will introduce Rust's procedural macro system by explaining the system as a whole.
Unlike a declarative macro, a procedural macro takes the form of a Rust function taking in a token stream(or two) and outputting a token stream.
A proc-macro is at its core just a function exported from a crate with the proc-macro
crate type, so when writing multiple proc macros you can have them all live in one crate.
Note: When using Cargo, to define a
proc-macro
crate you define and set thelib.proc-macro
key in theCargo.toml
to true.[lib] proc-macro = true
A proc-macro
type crate implicitly links to the compiler-provided proc_macro crate, which contains all the things you need to get going with developing procedural macros.
The two most important types exposed by the crate are the TokenStream
, which are the proc-macro variant of the already familiar token trees as well as the Span
, which describes a part of source code used primarily for error reporting and hygiene. See the Hygiene and Spans chapter for more information.
As proc-macros therefore are functions living in a crate, they can be addressed as all the other items in a Rust project. All thats required to add the crate to the dependency graph of a project and bring the desired item into scope.
Note: Procedural macros invocations still run at the same stage in the compiler expansion-wise as declarative macros, just that they are standalone Rust programs that the compiler compiles, runs, and finally either replaces or appends to.
Types of procedural macros
With procedural macros, there are actually exist 3 different kinds with each having slightly different properties.
- function-like proc-macros which are used to implement
$name ! $arg
invocable macros - attribute proc-macros which are used to implement
#[$arg]
attributes - derive proc-macros which are used to implement a derive, an input to a
#[derive(…)]
attribute
At their core, all 3 work almost the same with a few differences in their inputs and output reflected by their function definition. As mentioned all a procedural macro really is, is a function that maps a token stream so let's take a quick look at each basic definition and their differences.
function-like
#[proc_macro]
pub fn my_proc_macro(input: TokenStream) -> TokenStream {
TokenStream::new()
}
attribute
#[proc_macro_attribute]
pub fn my_attribute(input: TokenStream, annotated_item: TokenStream) -> TokenStream {
TokenStream::new()
}
derive
#[proc_macro_derive(MyDerive)]
pub fn my_derive(annotated_item: TokenStream) -> TokenStream {
TokenStream::new()
}
As shown, the basic structure is the same for each, a public function marked with an attribute defining its procedural macro type returning a TokenStream
.
Note how the return type is a TokenStream
and not a result or something else that gives the notion of being fallible.
This does not mean that proc-macros cannot fail though, in fact they have two ways of reporting errors, the first one being to panic and the second to emit a compile_error!
invocation.
If a proc-macro panics the compiler will catch it and emit the payload as an error coming from the macro invocation.
Beware: The compiler will happily hang on endless loops spun up inside proc-macros causing the compilation of crates using the proc-macro to hang as well.
Function-like
Function-like procedural macros are invoked like declarative macros that is makro!(…)
.
This type of macro is the simplest of the three though. It is also the only one which you can't differentiate from declarative macros when solely looking at the invocation.
A simple skeleton of a function-like procedural macro looks like the following:
use proc_macro::TokenStream;
#[proc_macro]
pub fn tlborm_fn_macro(input: TokenStream) -> TokenStream {
input
}
As one can see this is in fact just a mapping from one TokenStream
to another where the input
will be the tokens inside of the invocation delimiters, e.g. for an example invocation foo!(bar)
the input token stream would consist of the bar
token.
The returned token stream will replace the macro invocation.
For this macro type the same placement and expansion rules apply as for declarative macros, that is the macro must output a correct token stream for the invocation location. Unlike with declarative macros though, function-like procedural macros do not have certain restrictions imposed on their inputs though. That is the restrictions for what may follow fragment specifiers listed in the Metavariables and Expansion Redux chapter listed is not applicable here, as the procedural macros work on the tokens directly instead of matching them against fragment specifiers or similar.
With that said it is apparent that the procedural counter part to these macros is more powerful as they can arbitrarily modify their input, and produce any output desired as long as its within the bounds of the language syntax.
Usage example:
use tlborm_proc::tlborm_attribute;
fn foo() {
tlborm_attribute!(be quick; time is mana);
}
Attribute
Attribute procedural macros define new outer attributes which can be attached to items.
This type can be invoked with the #[attr]
or #[attr(…)]
syntax where …
is an arbitrary token tree.
A simple skeleton of an attribute procedural macro looks like the following:
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn tlborm_attribute(input: TokenStream, annotated_item: TokenStream) -> TokenStream {
annotated_item
}
Of note here is that unlike the other two procedural macro kinds, this one has two input parameters instead of one.
- The first parameter is the delimited token tree following the attribute's name, excluding the delimiters around it.
It is empty if the attribute is written bare, that is just a name without a
(TokenTree)
following it, e.g.#[attr]
. - The second token stream is the item the attribute is attached to without the attribute this proc macro defines.
As this is an
active
attribute, the attribute will be stripped from the item before it is being passed to the proc macro.
The returned token stream will replace the annotated item fully. Note that the replacement does not have to be a single item, it can be 0 or more.
Usage example:
use tlborm_proc::tlborm_attribute;
#[tlborm_attribute]
fn foo() {}
#[tlborm_attribute(attributes are pretty handsome)]
fn bar() {}
Derive
Derive procedural macros define new inputs for the derive
attribute.
This type can be invoked by feeding it to a derive attribute's input, e.g. #[derive(TlbormDerive)]
.
A simple skeleton of a derive procedural macro looks like the following:
use proc_macro::TokenStream;
#[proc_macro_derive(TlbormDerive)]
pub fn tlborm_derive(item: TokenStream) -> TokenStream {
TokenStream::neW()
}
The proc_macro_derive
is a bit more special in that it requires an extra identifier, this identifier will become the actual name of the derive proc macro.
The input token stream is the item the derive attribute is attached to, that is, it will always be an enum
, struct
or union
as these are the only items a derive attribute can annotate.
The returned token stream will be appended to the containing block or module of the annotated item with the requirement that the token stream consists of a set of valid items.
Usage example:
use tlborm_proc::TlbormDerive;
#[derive(TlbormDerive)]
struct Foo;
Helper Attributes
Derive proc macros are a bit more special in that they can add additional attributes visible only in the scope of the item definition.
These attributes are called derive macro helper attributes and are inert.
Their purpose is to give derive proc macros additional customizability on a per field or variant basis, that is these attributes can be used to annotate fields or enum variants while having no effect on their own.
As they are inert
they will not be stripped and are visible to all macros.
They can be defined by adding an attributes(helper0, helper1, ..)
argument to the proc_macro_derive
attribute containing a comma separated list of identifiers which are the names of the helper attributes.
Thus a simple skeleton of a derive procedural macro with helper attributes looks like the following:
use proc_macro::TokenStream;
#[proc_macro_derive(TlbormDerive, attributes(tlborm_helper))]
pub fn tlborm_derive(item: TokenStream) -> TokenStream {
TokenStream::new()
}
That is all there is to helper attributes, to consume them in the proc macro the implementation will then have to check the attributes of fields and variants to see whether they are attributed with the corresponding helper. It is an error to use a helper attribute if none of the used derive macros of the given item declare it as such, as the compiler will then instead try to resolve it as a normal attribute.
Usage example:
use tlborm_proc::TlbormDerive;
#[derive(TlbormDerive)]
struct Foo {
#[tlborm_helper]
field: u32
}
#[derive(TlbormDerive)]
enum Bar {
#[tlborm_helper]
Variant { #[tlborm_helper] field: u32 }
}
Third-Party Crates
Note: Crates beyond the automatically linked
proc_macro
crate are not required to write procedural macros. The crates listed here merely make writing them simpler and more concise, while potentially adding to the compilation time of the procedural macro due to added dependencies.
As procedural macros live in a crate they can naturally depend on (crates.io) crates. turns out the crate ecosystem has some really helpful crates tailored towards procedural macros that this chapter will quickly go over, most of which will be used in the following chapters to implement the example macros. As these are merely quick introductions it is advised to look at each crate's documentation for more in-depth information if required.
proc-macro2
proc-macro2
, the successor of the proc_macro
crate! Or so you might think but that is of course not correct, the name might be a bit misleading.
This crate is actually just a wrapper around the proc_macro
crate serving two specific purposes, taken from the documentation:
- Bring proc-macro-like functionality to other contexts like build.rs and main.rs.
- Make procedural macros unit testable.
As the proc_macro
crate is exclusive to proc_macro
type crates, making them unit testable or accessing them from non-proc macro code is next to impossible.
With that in mind the proc-macro2
crate mimics the original proc_macro
crate's api, acting as a wrapper in proc-macro crates and standing on its own in non-proc-macro crates.
Hence it is advised to build libraries targeting proc-macro code to be built against proc-macro2
instead as that will enable those libraries to be unit testable, which is also the reason why the following listed crates take and emit proc-macro2::TokenStream
s instead.
When a proc_macro
token stream is required, one can simply .into()
the proc-macro2
token stream to get the proc_macro
version and vice-versa.
Procedural macros using the proc-macro2
crate will usually import the proc-macro2::TokenStream
in an aliased form like use proc-macro2::TokenStream as TokenStream2
.
quote
The quote
crate mainly exposes just one macro, the quote!
macro.
This little macro allows you to easily create token streams by writing the actual source out as syntax while also giving you the power of interpolating tokens right into the written syntax.
Interpolation can be done by using the #local
syntax where local refers to a local in the current scope.
Likewise #( #local )*
can be used to interpolate over an iterator of types that implement ToTokens
, this works similar to declarative macro_rules!
repetitions in that they allow a separator as well as extra tokens inside the repetition.
let name = /* some identifier */;
let exprs = /* an iterator over expressions tokenstreams */;
let expanded = quote! {
impl SomeTrait for #name { // #name interpolates the name local from above
fn some_function(&self) -> usize {
#( #exprs )* // #name interpolates exprs by iterating the iterator
}
}
};
This a very useful tool when preparing macro output avoiding the need of creating a token stream by inserting tokens one by one.
Note: As stated earlier, this crate makes use of
proc_macro2
and thus thequote!
macro returns aproc-macro2::TokenStream
.
syn
The syn
crate is a parsing library for parsing a stream of Rust tokens into a syntax tree of Rust source code.
It is a very powerful library that makes parsing proc-macro input quite a bit easier, as the proc_macro
crate itself does not expose any kind of parsing capabilities, merely the tokens.
As the library can be a heavy compilation dependency, it makes heavy use of feature gates to allow users to cut it as small as required.
So what does it offer? A bunch of things.
First of all it has definitions and parsing for all standard Rust syntax nodes(when the full
feature is enabled), as well as a DeriveInput
type which encapsulates all the information a derive macro gets passed as an input stream as a structured input(requires the derive
feature, enabled by default). These can be used right out of the box with the parse_macro_input!
macro(requires the parsing
and proc-macro
features, enabled by default) to parse token streams into these types.
If Rust syntax doesn't cut it, and instead one wishes to parse custom non-Rust syntax the crate also offers a generic parsing API, mainly in the form of the Parse
trait(requires the parsing
feature, enabled by default).
Aside from this the types exposed by the library keep location information and spans which allows procedural macros to emit detailed error messages pointing at the macro input at the points of interest.
As this is again a library for procedural macros, it makes use of the proc_macro2
token streams and spans and as such, conversions may be required.
Hygiene and Spans
This chapter talks about procedural macro hygiene and the type that encodes it, Span
.
Every token in a TokenStream
has an associated Span
holding some additional info.
A span, as its documentation states, is A region of source code, along with macro expansion information
.
It points into a region of the original source code(important for displaying diagnostics at the correct places) as well as holding the kind of hygiene for this location.
The hygiene is relevant mainly for identifiers, as it allows or forbids the identifier from referencing things or being referenced by things defined outside of the invocation.
There are 3 kinds of hygiene(which can be seen by the constructors of the Span
type):
definition site
(unstable): A span that resolves at the macro definition site. Identifiers with this span will not be able to reference things defined outside or be referenced by things outside of the invocation. This is what one would call "hygienic".mixed site
: A span that has the same hygiene asmacro_rules
declarative macros, that is it may resolve to definition site or call site depending on the type of identifier. See here for more information.call site
: A span that resolves to the invocation site. Identifiers in this case will behave as if written directly at the call site, that is they freely resolve to things defined outside of the invocation and can be referenced from the outside as well. This is what one would call "unhygienic".
用語集
分かりにくい言葉とその意味をまとめる場所です。 重要な言葉が抜けていると思った方は、issueやプルリクエストを立てていただけると幸いです。
関数形式マクロ (Function-like macro)
関数形式マクロは、identifier!(...)
という形で発動できる構文拡張を記述するものです。
関数呼び出しに似ていることからこのように呼ばれています。
構文拡張 (Syntax Extension)
Rustのmacro_rules!
や手続き的マクロの基礎となる機構。