14.5. マクロ(Macros)🔗
マクロ (macro)は エラボレーション および タクティクの実行 中に出現する Syntax
から Syntax
への変換処理のことです。構文をマクロで変換した結果で置き換えることを マクロ展開 (macro expansion)と呼びます。複数のマクロを1つの 構文種別 に関連付けることができ、定義順に試行されます。マクロは monad の中で実行され、コンパイル時のメタデータにアクセスし、エラーメッセージを出すかマクロ展開を後続のマクロに委譲するする機能を持ちますが、マクロモナドはエラボレーションモナドにくらべてはるかに非力です。
マクロは 構文種別 に関連付けられます。内部テーブルは構文種別を Syntax → MacroM Syntax
型のマクロにマッピングします。マクロは unsupportedSyntax
例外を投げることで次のエントリに委譲します。与えられた Syntax
の値は unsupportedSyntax
を投げない構文種別に関連付けられたマクロが存在する場合、 マクロとなります 。マクロがそれ以外の例外を投げる場合、ユーザにエラーが報告されます。 構文カテゴリ はマクロ展開には関係ありません;ただし、各構文種別は通常1つの構文カテゴリに関連付けられているため実際には干渉しません。
マクロからのエラー報告
次のマクロはパラメータがリテラル数値 5 である場合にエラーを報告します。それ以外の場合は引数に展開されます。
syntax &"notFive" term:arg : term
open Lean in
macro_rules
| `(term|notFive 5) =>
Macro.throwError "'5' is not allowed here"
| `(term|notFive $e) =>
pure e
構文的に数値の 5 でない項に適用するとエラボレーションが成功します:
5
#eval notFive (2 + 3)
エラーのケースがトリガーされると、ユーザはエラーメッセージを受け取ります:
#eval '5' is not allowed here
notFive 5
構文の一部をエラボレートする前に、エラボレータはその 構文種別 にマクロが関連付けられているかどうかをチェックします。これらは順番に試行されます。マクロが成功し、異なる種類の構文を返す可能性がある場合、チェックが繰り返され、構文の一番外側がマクロでなくなるまでマクロが繰り返し展開されます。その後、エラボレーションやタクティクの実行が可能になります。構文の一番外側(通常は node
)だけが展開されます。このマクロ展開の出力にはマクロの構文が含まれる場合があります。これらの入れ子になったマクロはエラボレータがそこに到達した時に順番に展開されます。
特に、Lean では3つの状況でマクロ展開が発生します:
-
項のエラボレーションの際において、 構文の項エラボレータ を呼び出す前にエラボレートされる構文の一番外側のマクロが展開されます。
-
コマンドのエラボレーションの際において、 構文のコマンドエラボレータ を呼び出す前にエラボレートされる一番外側のマクロが展開されます。
-
タクティクの実行の際において、 その構文をタクティクとして実行する前に エラボレートされる一番外側のマクロが展開されます。
14.5.1. 健全性(Hygiene)🔗
マクロが 健全である (hygienic)であるとは、その展開によって識別子のキャプチャが生じないようなマクロを指します。 識別子のキャプチャ (identifier capture)とは、識別子がソースコード内で発生するスコープ以外の束縛位置を参照してしまうことです。識別子のキャプチャには2つのタイプがあります:
1つ目の変数キャプチャは、マクロによって導入されるすべての束縛子が新しく生成されたもので、かつグローバルで一意な名前を使用することで、2つ目は定数の参照に常に完全修飾名を使用することでそれぞれ回避できます。1つ目の対策での新しい名前は、再帰的なマクロでの変数キャプチャを回避するためにマクロの呼び出しのたびに生成する必要があります。これらのテクニックはエラーを起こしやすいです。変数キャプチャの問題は、名前の選択の一致に依存するためテストが難しく、これらのテクニックを一貫して適用すると読みづらいコードになります。
Lean は自動的な健全性の機構を持ちます:ほとんどすべての場合において、マクロは自動的に健全です。導入された束縛子によるキャプチャは、マクロによって導入された識別子を マクロスコープ (macro scope)で注釈することで回避できます。マクロスコープはマクロ展開のそれぞれの呼び出しごとに一意に識別されます。束縛子と識別子の使用が同じマクロスコープを持つ場合、それらはマクロ展開の同じステップで導入されたものであり、お互いを参照する必要があります。同様に、マクロによって生成されたコードでのグローバル名の使用は、それが展開されたコンテキストではローカルの束縛子によってキャプチャされません。なぜなら、そのグローバル名の使用位置には束縛子の出現には存在しないマクロスコープがあるからです。新しく導入されたグローバル名によるキャプチャは、潜在的なグローバル名参照に対してマクロの本体で生成されるコードで quotation 時に一致するグローバル名のセットをアノテーションすることで防がれます。潜在的な参照による注釈された識別子は 事前解決された識別子 (pre-resolved identifier)と呼ばれ、 Syntax.ident
コンストラクタの Syntax.Preresolved
フィールドは潜在的な参照先を格納するために使用されます。エラボレーションの際に、識別子が事前解決されたグローバル名に関連付けられている場合、他のグローバル名は有効な参照対象として考慮されません。
生成された構文へのマクロスコープと事前解決された識別子の導入は quotation の間に行われます。quotation 以外の方法で構文を構築するマクロであっても、他の何らかの方法で健全性を確保する必要があります。Lean の健全性アルゴリズムの詳細については Ullrich and de Moura (2020)Sebastian Ullrich and Leonardo de Moura, 2020. “Beyond notations: Hygienic macro expansion for theorem proving languages”. In Proceedings of the International Joint Conference on Automated Reasoning. and Ullrich (2023)Sebastian Ullrich, 2023. An Extensible Theorem Proving Frontend. Dr. Ing. dissertation, Karlsruhe Institute of Technology を参照してください。
14.5.2. マクロモナド(The Macro Monad)🔗
マクロモナド MacroM
は健全性を実装し、エラーを報告するのに十分な機能を有しています。マクロ展開には環境を直接変更したり、単一化を実行したり現在のローカルコンテキストを調べたり、ある特定のコンテキストのみで意味を持つようなことはできません。これにより、Lean 全体で同じマクロ機構を使用することができ、マクロは エラボレータ よりもはるかに書きやすくなります。
🔗defLean.MacroM (α : Type) : Type
The MacroM
monad is the main monad for macro expansion. It has the
information needed to handle hygienic name generation, and is the monad that
macro
definitions live in.
Notably, this is a (relatively) pure monad: there is no IO
and no access to
the Environment
. That means that things like declaration lookup are
impossible here, as well as IO.Ref
or other side-effecting operations.
For more capabilities, macros can instead be written as elab
using adaptExpander
.
🔗defLean.Macro.trace (clsName : Lean.Name)
(msg : String) : Lean.MacroM Unit
Add a new trace message, with the given trace class and message.
14.5.2.1. 例外とエラー(Exceptions and Errors)🔗
unsupportedSyntax
例外は、マクロ展開時の制御フローに使用されます。これは現在のマクロが受け取った構文を展開できないが、エラーが発生したわけではないことを示します。 throwError
と throwErrorAt
が投げる例外はマクロ展開を終了し、ユーザにエラーを報告します。
🔗defLean.Macro.throwUnsupported {α : Type}
: Lean.MacroM α
Throw an unsupportedSyntax
exception.
🔗Lean.Macro.Exception.unsupportedSyntax
: Lean.Macro.Exception
An unsupported syntax exception. We keep this separate because it is
used for control flow: if one macro does not support a syntax then we try
the next one.
🔗defLean.Macro.throwError {α : Type} (msg : String)
: Lean.MacroM α
Throw an error with the given message,
using the ref
for the location information.
🔗defLean.Macro.throwErrorAt {α : Type}
(ref : Lean.Syntax) (msg : String)
: Lean.MacroM α
Throw an error with the given message and location information.
14.5.2.2. 健全性関連の操作(Hygiene-Related Operations)🔗
Hygiene は構文中に出現する識別子に マクロスコープ を追加することで実装されます。通常、 quotation の処理は必要なスコープをすべて追加しますが、構文を直接構成するマクロは、導入する識別子にマクロスコープを追加しなければなりません。
🔗defLean.Macro.withFreshMacroScope {α : Type}
(x : Lean.MacroM α) : Lean.MacroM α
Increments the macro scope counter so that inside the body of x
the macro
scope is fresh.
🔗defLean.Macro.addMacroScope (n : Lean.Name)
: Lean.MacroM Lean.Name
Add a new macro scope to the name n
.
14.5.2.3. 環境への問い合わせ(Querying the Environment)🔗
マクロの環境への問い合わせは限られた機能しかサポートされていません。定数が存在するかどうかをチェックし、名前を解決することはできますが、それ以上の考慮・吟味はできません。
🔗defLean.Macro.hasDecl (declName : Lean.Name)
: Lean.MacroM Bool
Returns true
if the environment contains a declaration with name declName
🔗defLean.Macro.getCurrNamespace
: Lean.MacroM Lean.Name
Gets the current namespace given the position in the file.
🔗defLean.Macro.resolveNamespace (n : Lean.Name)
: Lean.MacroM (List Lean.Name)
Resolves the given name to an overload list of namespaces.
🔗defLean.Macro.resolveGlobalName (n : Lean.Name)
: Lean.MacroM
(List (Lean.Name × List String))
Resolves the given name to an overload list of global definitions.
The List String
in each alternative is the deduced list of projections
(which are ambiguous with name components).
Remark: it will not trigger actions associated with reserved names. Recall that Lean
has reserved names. For example, a definition foo
has a reserved name foo.def
for theorem
containing stating that foo
is equal to its definition. The action associated with foo.def
automatically proves the theorem. At the macro level, the name is resolved, but the action is not
executed. The actions are executed by the elaborator when converting Syntax
into Expr
.
14.5.3. Quotation🔗
Quotation は Syntax
型のデータとして表現するためにコードをマークします。quote されたコードはパースされますが、エラボレートはされません。つまり構文的に正しくなければなりませんが、意味を為している必要はありません。quotation はコードをプログラム的に生成することをとても簡単にします:Lean のパーサが生成する node
値の特定の入れ子をリバースエンジニアリングするのではなく、パーサを直接呼び出して生成することができます。これはまた、文法のリファクタリングが、ユーザから見える具体的な構文に影響を与えることなくパースされた木の内部を変更する可能性がある場合により堅牢です。Lean の quotation は `(
と )
で囲まれます。
quote される構文カテゴリやパーサは、その名前を先頭のバックティックと括弧の後に置き、その後に縦棒(|
)を置くことで示すことができます。特殊なケースとして、tactic
という名前はタクティクやタクティクの列をパースするために使用することができます。構文カテゴリやパーサが提供されていない場合、Lean は quotation を項と空でない一連のコマンドの両方としてパースしようとします。項の quotation はコマンドの quotation よりも優先順位が高いため、あいまいな場合は項として解釈されます;これはコマンド列の quotation であることを明示することで上書きできます。
項 vs コマンドの quotation 構文
以下の例では、quotation の中身は関数適用かコマンド列のどちらかです。どちらもファイルの同じ領域でマッチするため、 ローカル最長一致規則 は関係しません。項の quotation はコマンドの quotation よりも優先順位が高いため、quotation は項として解釈されます。項は TSyntax `command
型よりも TSyntax `term
型を持つ antiquotations を期待します。
example (cmd1 cmd2 : TSyntax `command) : MacroM (TSyntax `command) := `($application type mismatch
cmd1.raw
argument
cmd1
has type
TSyntax `command : Type
but is expected to have type
TSyntax `term : Type
cmd1 $application type mismatch
cmd2.raw
argument
cmd2
has type
TSyntax `command : Type
but is expected to have type
TSyntax `term : Type
cmd2)
これにより、以下のような2つの型エラーが発生します:
application type mismatch
cmd1.raw
argument
cmd1
has type
TSyntax `command : Type
but is expected to have type
TSyntax `term : Type
構文の優先順位はエラボレーションの前に適用されるため、quotation の型( MacroM (TSyntax `command)
)は結果の選択に使用されません。この場合、antiquotation がコマンドであることを指定すると、関数の適用がこれらの位置に項を必要とするため、あいまいさが解決されます:
example (cmd1 cmd2 : TSyntax `command) : MacroM (TSyntax `command) := `($cmd1:command $cmd2:command)
同様に、コマンドを quotation の中に挿入することができ、それが項である可能性を排除することができます:
example (cmd1 cmd2 : TSyntax `command) : MacroM (TSyntax `command) := `($cmd1 $cmd2 #eval "hello!")
syntax
Lean の構文には、項・コマンド・タクティク・タクティク列の quotation と、Lean がパースできるすべての入力を quote できる一般的な quotation 構文があります。項の quotation が最も優先順位が高く、次いでタクティクの quotation・一般的な quotation・コマンドの quotation と続きます。
term ::=
Syntax quotation for terms.
`(term)
| `(command+)
| `(tactic|tactic)
| `(tactic|tactic;*)
| `(p : ident
p:ident|)
quotation は Syntax
型ではなく、 m Syntax
型を持ったモナドアクションです。 健全性の節 で説明されているように、 マクロスコープ と事前解決された識別子を追加することで hygiene を実装しているため、quotation はモナドです。使用する特定のモナドは quotation の暗黙のパラメータであり、 MonadQuotation
型クラスのインスタンスを持つ任意のモナドが適しています。 MonadQuotation
は MonadRef
を継承しており、マクロ展開またはエラボレータが現在処理している構文のソース位置にアクセスすることができます。 MonadQuotation
はさらに識別子に マクロスコープ を追加し、サブタスクに新しいマクロスコープを使用する機能を含んでいます。quotation をサポートするモナドは、 MacroM
・ TermElabM
・ CommandElabM
・ TacticM
です。
14.5.3.1. Quasiquotation🔗
Quasiquotation は antiquotations を含むことができる quotation の形式です。antiquotation は、quote されていない代わりに評価すると構文を生成する式である quotation の領域を指します。quasiquotation は本質的にはテンプレートです;外側の quote された領域は、常に同じ外側の構文を生成する固定されたフレームワークを提供します。Lean の quotation はすべて quasiquotation であるため、quasiquotation を他の quotation と区別するための特別な構文は必要ありません。quotation 処理は antiquotation を通じて挿入された識別子にマクロスコープを追加しません。なぜなら、これらの識別子は別の quotation から来るか(その場合すでにマクロスコープを持っています)、マクロの入力から来るか(その場合、マクロによって導入されたものではないためマクロスコープを持つべきではありません)のいずれかであるからです。
基本的な antiquotation はドル記号($
)の直後に識別子が続くものです。これは構文木であるべき対応する変数の値が、quote された構文のこの位置に代入されることを意味します。式全体を括弧でくくることで、 antiquotation として使用することができます。
Lean のパーサは、パーサが指定された位置で何を期待するかに基づいて、すべての antiquotation に構文カテゴリを割り当てます。パーサが構文カテゴリ c
を期待する場合、antiquotation の型は TSyntax c
になります。
構文カテゴリの中には、他のカテゴリの要素とマッチするものがあります。例えば、数値リテラルと文字列リテラルは、それ自身の構文カテゴリであることに加えて、有効な項です。antiquotation はコロンとカテゴリ名を接尾辞として付けることで、期待されるカテゴリを注釈することができます。これにより、パーサは注釈されたカテゴリが指定された位置で受け入れられるかどうか検証し、パースされた木で必要とされる中間層を構築します。
syntaxAntiquotations
antiquot ::=
$ident(:ident)?
| $(term)(:ident)?
antiquotation を開始するドル記号($
)と、それに続く識別子または括弧で囲まれた項の間に空白を入れることはできません。同様に、antiquotation の構文カテゴリを示すコロンの周りには空白を入れられません。
Quasiquotation
この例では antiquotation の両方の形式が使われています。自然数は構文ではないため、 quote
は数値を表す構文に変換するために使用されます。
open Lean in
example [Monad m] [MonadQuotation m] (x : Term) (n : Nat) : m Syntax :=
`($x + $(quote (n + 2)))
Antiquotation 注釈
この例では m
が quotation を行えるモナドであることを求めています。
variable {m : Type → Type} [Monad m] [MonadQuotation m]
デフォルトでは、antiquotation $e
は項であることが期待されます。なぜなら、それが加算の第2引数としてすぐに期待される構文カテゴリであるからです。
def ex1 (e) := show m _ from `(2 + $e)
ex1 {m : Type → Type} [Monad m] [MonadQuotation m] (e : TSyntax `term) : m (TSyntax `term)
#check ex1
数値リテラルも有効な項であるため、$e
の数値リテラルとして注釈は成功します。パラメータ e
に期待される型は TSyntax `num
に変更されます。
def ex2 (e) := show m _ from `(2 + $e:num)
ex2 {m : Type → Type} [Monad m] [MonadQuotation m] (e : TSyntax `num) : m (TSyntax `term)
#check ex2
ドル記号と識別子の間にスペースを入れてはいけません。
def ex2 (e) := show m _ from `(2 + expected '`(tactic|' or no space before spliced term
$ e:num)
<example>:1:35: expected '`(tactic|' or no space before spliced term
また、コロンの前にスペースを入れることもできません:
def ex2 (e) := show m _ from `(2 + $e expected ')'
:num)
<example>:1:38: expected ')'
Quasiquotation の展開
f
の定義を表示することで、quasiquotation の展開が示されます。
open Lean in
def f [Monad m] [MonadQuotation m] (x : Term) (n : Nat) : m Syntax :=
`(fun k => $x + $(quote (n + 2)) + k)
def f : {m : Type → Type} → [inst : Monad m] → [inst : Lean.MonadQuotation m] → Lean.Term → Nat → m Syntax :=
fun {m} [Monad m] [Lean.MonadQuotation m] x n => do
let info ← Lean.MonadRef.mkInfoFromRefPos
let scp ← Lean.getCurrMacroScope
let mainModule ← Lean.getMainModule
pure
{
raw :=
Syntax.node2 info `Lean.Parser.Term.fun (Syntax.atom info "fun")
(Syntax.node4 info `Lean.Parser.Term.basicFun
(Syntax.node1 info `null (Syntax.ident info "k".toSubstring' (Lean.addMacroScope mainModule `k scp) []))
(Syntax.node info `null #[]) (Syntax.atom info "=>")
(Syntax.node3 info `«term_+_»
(Syntax.node3 info `«term_+_» x.raw (Syntax.atom info "+") (Lean.quote `term (n + 2)).raw)
(Syntax.atom info "+")
(Syntax.ident info "k".toSubstring' (Lean.addMacroScope mainModule `k scp) []))) }.raw
#print f
この出力では、quotation は Lean.Parser.Term.do : term
do
ブロックです。これは、処理中の現在のユーザ構文についてコンパイラに問い合わせることで得られる結果の構文のソース情報を構築することから始まります。次に、現在のマクロスコープと処理対象のモジュール名を取得します。これはマクロスコープはモジュールに対して追加されるため、独立したコンパイルが可能になり、グローバルカウンタが不要になるからです。次に、 Syntax.node1
や Syntax.node2
などの補助関数を使用してノードを構築します。これらの補助関数は指定された数の子を持つ Syntax.node
を作成します。マクロスコープは各識別子に追加され、 TSyntax.raw
は型付き構文ラッパーの内容を抽出するために使用されます。 x
と quote (n + 2)
の antiquotation は Syntax.node3
のパラメータとして展開の中で直接発生します。
14.5.3.2. スプライス(Splices)🔗
quasiquotation には antiquotation による他の構文に加えて、 スプライス (splice)を含めることができます。スプライスは配列の要素を順番に挿入することを示します。繰り返される要素には、リストや配列の要素間のカンマのような区切り文字を含めることができます。スプライスは スプライス接尾辞 (splice suffix)を持つ通常の antiquotation から構成されることもあれば、追加の繰り返し構造を提供する 拡張スプライス (extended splice)であることもあります。
スプライス接尾辞は、アスタリスクまたは有効なアトムの後にアスタリスク(*
)を付けたものです。接尾辞は任意の識別子または項の antiquotation の後につけることができます。スプライス接尾辞 *
を持つ antiquotation は many
または many1
に対応します;構文規則の *
と +
接尾辞のどちらも *
スプライス接尾辞に対応します。アスタリスクの前にアトムを含むスプライス接尾辞を持つ antiquotation は sepBy
または sepBy1
に対応します。スプライス接尾辞 ?
は構文規則の optional
または ?
接尾辞の使用に対応します。?
は有効な識別子文字であるため、これを接尾辞として使用するには括弧でくくらなければなりません。
構文の繰り返し指定子と antiquotation の接尾辞は重複していますが、それぞれ別の構文を持っています。構文を定義する際、接尾辞 *
・ +
・ ,*
・ ,+
・ ,*,?
・ ,+,?
は Lean に組み込まれています。区切り文字を ,
以外に短く指定する方法はありません。antiquotation の接尾辞は *
だけか、sepBy
または sepBy1
に指定されたアトムの後に *
が続くものです。構文の繰り返し +
と *
はスプライス接尾辞 *
に対応し、繰り返し ,*
・ ,+
・ ,*,?
・ ,+,?
は ,*
に対応します。構文とスプライスのオプションの接尾辞 ?
はどちらも同じものに対応します。
Syntax Repetition
|
Splice Suffix
|
---|
+ *
|
*
|
,* ,+ ,*,? ,+,?
|
,*
|
sepBy(_, "S") sepBy1(_, "S")
|
S*
|
?
|
?
|
接尾辞のついたスプライス
この例では、 m
が quotation を実行できるモナドであることを要求しています。
variable {m : Type → Type} [Monad m] [MonadQuotation m]
デフォルトでは、$e
はリストの本体で期待されるように、カンマで区切られた項の配列であると期待されます:
def ex1 (xs) := show m _ from `(#[$xs,*])
ex1 {m : Type → Type} [Monad m] [MonadQuotation m] (xs : Syntax.TSepArray `term ",") : m (TSyntax `term)
#check ex1
しかし、Lean には配列の様々な表現の間に自動的に区切り文字を挿入したり削除したりする強制のコレクションが含まれているため、普通の項の配列でも構いません:
def ex2 (xs : Array (TSyntax `term)) := show m _ from `(#[$xs,*])
ex2 {m : Type → Type} [Monad m] [MonadQuotation m] (xs : Array (TSyntax `term)) : m (TSyntax `term)
#check ex2
繰り返しの注釈は項の antiquotation や構文カテゴリの注釈と一緒に使用することもできます。この例は CommandElabM
で記述されているため、結果を便利にログに残すことができます。
def ex3 (size : Nat) := show CommandElabM _ from do
let mut nums : Array Nat := #[]
for i in [0:size] do
nums := nums.push i
let stx ← `(#[$(nums.map (Syntax.mkNumLit ∘ toString)):num,*])
-- Using logInfo here causes the syntax to be rendered via the pretty printer.
logInfo stx
#[0, 1, 2, 3]
#eval ex3 4
カンマではない区切り文字
以下のリスト用の型破りな構文は、数値要素をカンマで区切るのではなく、em ダッシュまたは2つのアスタリスクで区切ります。
syntax "⟦" sepBy1(num, " — ") "⟧": term
syntax "⟦" sepBy1(num, " ** ") "⟧": term
これは —*
と ***
がアトム ⟦
と ⟧
の間の有効なスプライス接尾辞であることを意味します。***
の場合、最初の2つのアスタリスクは構文規則のアトムで、3つ目は反復の接尾辞になります。
macro_rules
| `(⟦$n:num—*⟧) => `(⟦$n***⟧)
| `(⟦$n:num***⟧) => `([$n,*])
[1, 2, 3]
#eval ⟦1 — 2 — 3⟧
オプショナルなスプライス
以下の構文宣言は、2つのトークンの間にある項にオプション的にマッチします。入れ子になった term
を括弧で囲む必要があるのは、 term?
が有効な識別子だからです。
syntax "⟨| " (term)? " |⟩": term
項のスプライス接尾辞 ?
は Option Term
を期待します:
def mkStx [Monad m] [MonadQuotation m] (failed to infer binder type
when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed
e) : m Term :=
`(⟨| $(e)? |⟩)
mkStx {m : Type → Type} [Monad m] [MonadQuotation m] (e : Option (TSyntax `term)) : m Term
#check mkStx
some
を指定すると、オプショナルな項が存在することになります。
⟨| 5 |⟩
#eval do logInfo (← mkStx (some (quote 5)))
none
を指定すると、オプショナルな項が存在s内ことになります。
⟨| |⟩
#eval do logInfo (← mkStx none)
14.5.3.3. トークンの antiquotation(Token Antiquotations)🔗
完全な構文の antiquotation に加えて、Lean は トークンの antiquotation (token antiquotation)を備えています。これはアトムのソース情報を他の構文のソース情報に置き換えることができます。これは主に、Lean がユーザに報告するエラーメッセージやその他の情報の配置を制御するのに便利です。トークンの antiquotation では、評価によって任意のアトムを挿入することはできません。トークンの antiquotation はアトム(つまりキーワード)から構成されます。
syntaxToken Antiquotations
トークンの antiquotation はトークンのソース情報( SourceInfo
型)を他の構文のソース情報に置き換えます。
antiquot ::= ...
| atom%$ident
14.5.4. 構文のマッチ(Matching Syntax)🔗
quasiquotation はテンプレートにマッチする構文を認識するためのパターンマッチで使用することができます。項として使用される quotation の中の antiquotation が通常の非 quote 式として扱われる領域であるのと同じように、パターンの中の antiquotation の領域は通常の Lean のパターンとして扱われます。quote パターンは他のパターンとは異なる方法でコンパイルされるため、1つの Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
式の中に quote パターン以外のパターンを混在させることはできません。通常の quotation と同様に、quote パターンはまず Lean のパーサによって処理されます。そしてパーサの出力はマッチするかどうかを判断するコードにコンパイルされます。構文マッチは、マッチする構文が Lean のパーサによって quotation を経由・または直接ユーザコードで生成されたものであると仮定し、いくつかのチェックを省略するためにこれを使用します。例えば、ある位置に特定のキーワード以外が存在しない場合、そのチェックは省略されます。
構文は以下の場合に quote パターンにマッチします:
- アトム
キーワードアトム( termIfThenElse : term
`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to
return `t` or `e` depending on whether `c` is true or false. The explicit argument
`c : Prop` does not have any actual computational content, but there is an additional
`[Decidable c]` argument synthesized by typeclass inference which actually
determines how to evaluate `c` to true or false. Write `if h : c then t else e`
instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact
that `c` is true/false.
if
や Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
など)は token.
の後にアトムが続く、子を1つだけ持つノードとなります。多くの場合、文法は単一のキーワードしか許さないため、特定のアトムの値をチェックする必要はなく、チェックは行われません。マッチする語の構文がチェックを必要とする場合は、ノードの種別を比較します。
文字列や数値リテラルなどのリテラルは、その文字列表現によって比較されます。パターン `(0x15)
と quotation `(21)
はマッチしません。
- ノード
マッチするパターンと値の両方が Syntax.node
を表す場合、両方が同じ構文種別・同じ数の子を持ち、それぞれの子のパターンが対応する子の値にマッチする時にマッチします。
- 識別子
パターンとマッチする値の両方が識別子の場合、そのリテラル Name
の値はマクロスコープで等しいかどうか比較されます。同じように「見える」識別子はマッチし、それらが同じ束縛子を参照しているかどうかは問題になりません。この設計上の選択により、名前を参照で比較できるコンパイル時の環境に対してアクセスできないコンテキストでも quote パターンマッチを使用できるようになります。
quotation のパターンマッチはパーサが出力するノードの種別に基づいて行われるため、同じように見える quotation でも構文カテゴリが異なるとマッチしないことがあります。疑わしい場合は、quotation に構文カテゴリを含めると良いでしょう。
14.5.5. マクロの定義(Defining Macros)🔗
マクロを定義するには主に2つの方法があります: Lean.Parser.Command.macro_rules : command
macro_rules
コマンドと Lean.Parser.Command.macro : command
macro
コマンドです。 Lean.Parser.Command.macro_rules : command
macro_rules
コマンドは既存の構文にマクロを関連付けますが、 Lean.Parser.Command.macro : command
macro
コマンドは新しい構文とそれを既存の構文に変換するマクロを同時に定義します。 Lean.Parser.Command.macro : command
macro
コマンドは Lean.Parser.Command.notation : command
notation
を一般化したものと見なすことができます。これは単純に置換するのではなく、プログラム的に生成された展開を許可するものです。
14.5.5.1. macro_rules
コマンド(The macro_rules
Command)🔗
syntax
Lean.Parser.Command.macro_rules : command
macro_rules
コマンドは、構文のパターンマッチとして指定された一連の書き換えルールを受け取り、それぞれをマクロとして追加します。ルールは以前に定義されたマクロの前に順番に試行され、後のマクロ定義でさらにマクロルールを追加することができます。
command ::= ...
| A `docComment` parses a "documentation comment" like `/-- foo -/`. This is not treated like
a regular comment (that is, as whitespace); it is parsed and forms part of the syntax tree structure.
A `docComment` node contains a `/--` atom and then the remainder of the comment, `foo -/` in this
example. Use `TSyntax.getDocString` to extract the body text from a doc string syntax node.
docComment?
(@[attrInstance,*])?
`attrKind` matches `("scoped" <|> "local")?`, used before an attribute like `@[local simp]`.
attrKind macro_rules ((kind := ident))?
(| Syntax quotation for terms.
`((p : ident
p:ident|)?) => term)*
マクロのパターンは quotation パターンでなければなりません。これらはどの構文カテゴリの構文にもマッチしますが、1つのパターンがマッチするのは1つの構文種別だけです。quotation にカテゴリやパーサが指定されていない場合、それは項かコマンド(の列)にマッチしますが、両方にマッチすることはありません。あいまいな場合は、項のパーサが選択されます。
内部的には、マクロは各 構文種別 をそのマクロに対応付けるテーブルで追跡されます。 Lean.Parser.Command.macro_rules : command
macro_rules
コマンドは構文種別を明示的に注釈することができます。
構文種別が明示的に指定されている場合、マクロ定義は各 quotation パターンがその種別を持つかどうかをチェックします。quotation のパース結果が choice node であった場合(つまりパースが曖昧であった場合)、指定された種別を持つ各選択肢に対してパターンが1回複製されます。指定された種別を持つ選択肢が1つも無い場合はエラーとなります。
種別を明示的に指定しない場合、パーサが決定した種別が各パターンに使用されます。パターンがすべて同じ構文種別を持つ必要はありません;少なくとも1つのパターンで使用される構文種別ごとにマクロが定義されます。quotation パターンのパース結果が choice node であった場合(つまりパース結果が曖昧であった場合)はエラーとなります。
構文自体にドキュメントコメントが無い場合、 Lean.Parser.Command.macro_rules : command
macro_rules
に関連付けられたドキュメントコメントがユーザに表示されます。そうでない場合は、構文自体のドキュメントコメントが表示されます。
notations と operators と同様に、マクロ規則も scoped
や local
として宣言することができます。スコープ付きマクロは現在の名前空間が開いているときにのみ有効であり、ローカルなマクロ規則は現在の セクションスコープ においてのみ有効です。
Idiom Brackets
idiom bracket はアプリカティブ関手を扱うための代替構文です。idiom bracket に関数適用が含まれている場合、関数は pure
でラップされ、<*>
を使って各引数に適用されます。Lean はデフォルトでは idiom bracket をサポートしていませんが、マクロを使って定義することができます。
syntax (name := idiom) "⟦" (term:arg)+ "⟧" : term
macro_rules
| `(⟦$f $args*⟧) => do
let mut out ← `(pure $f)
for arg in args do
out ← `($out <*> $arg)
return out
この新しい構文は直ちに使うことができます。
def addFirstThird [Add α] (xs : List α) : Option α :=
⟦Add.add xs[0]? xs[2]?⟧
none
#eval addFirstThird (α := Nat) []
none
#eval addFirstThird [1]
some 4
#eval addFirstThird [1,2,3,4]
スコープ付きマクロ
スコープ付きマクロ規則はその名前空間でのみ有効です。名前空間 ConfusingNumbers
が開いている場合、数値リテラルは正しくない意味になります。
namespace ConfusingNumbers
以下のマクロは奇数の数値リテラルである項を認識し、その値を2倍したものに置き換えます。もし無条件に2倍に置き換えてしまうと、同じ規則が必ず出力にマッチしてしまうため、マクロ展開は無限ループになってしまいます。
scoped macro_rules
| `($n:num) => do
if n.getNat % 2 = 0 then Lean.Macro.throwUnsupported
let n' := (n.getNat * 2)
`($(Syntax.mkNumLit (info := n.raw.getHeadInfo) (toString n')))
名前空間が閉じられると、このマクロはもはや使うことができません。
end ConfusingNumbers
名前空間を開かない場合、数値リテラルは通常通り機能します。
(3, 4)
#eval (3, 4)
名前空間が開かれると、マクロは 3
を 6
に置き換えます。
open ConfusingNumbers
(6, 4)
#eval (3, 4)
数値リテラルやその他のリテラルの解釈をマクロで変更することは通常は有用ではありません。しかし、スコープ付きマクロは、 trivial
のような拡張可能なタクティクに新しい規則を追加する時に非常に便利です。
裏側では、 Lean.Parser.Command.macro_rules : command
macro_rules
コマンドが quote パターンでマッチした構文種別ごとに1つのマクロ関数を生成します。この関数にはデフォルトケースとして unsupportedSyntax
例外を投げるケースを持つため、追加のマクロを試行することが可能です。
2つの規則を持つ単一の Lean.Parser.Command.macro_rules : command
macro_rules
コマンドは、必ずしも2つの単一ケースにマッチするコマンドと同じとは限りません。まず、 Lean.Parser.Command.macro_rules : command
macro_rules
の規則は上から下に試行されますが、直近で宣言されたマクロが最初に試行されるため、同じ挙動にするには順序を逆にする必要があります。さらに、マクロ内の以前の規則が unsupportedSyntax
例外を投げた場合、それ以降の規則は試されません;規則が別々の Lean.Parser.Command.macro_rules : command
macro_rules
コマンドであった場合は試行されます。
1つ vs 2つのマクロ規則
arbitrary!
マクロは与えられた型に対して任意に決定される値を展開することを意図しています。
syntax (name := arbitrary!) "arbitrary!" term:arg : term
macro_rules
| `(arbitrary! ()) => `(())
| `(arbitrary! Nat) => `(42)
| `(arbitrary! ($t1 × $t2)) => `((arbitrary! $t1, arbitrary! $t2))
| `(arbitrary! Nat) => `(0)
ユーザはさらにマクロ規則を定義することでこれを拡張できます。例えばこの Empty
に対する規則は失敗します:
macro_rules
| `(arbitrary! Empty) => throwUnsupported
(42, 42)
#eval arbitrary! (Nat × Nat)
すべてのマクロ規則が個別のケースとして定義されていた場合、上記は Nat
の後に定義されたケースを使用したことでしょう。これは1つの Lean.Parser.Command.macro_rules : command
macro_rules
コマンドの規則は上から下にチェックされますが、より最近定義された Lean.Parser.Command.macro_rules : command
macro_rules
コマンドがより前のものより優先されるためです。
macro_rules
| `(arbitrary! ()) => `(())
macro_rules
| `(arbitrary! Nat) => `(42)
macro_rules
| `(arbitrary! ($t1 × $t2)) => `((arbitrary! $t1, arbitrary! $t2))
macro_rules
| `(arbitrary! Nat) => `(0)
macro_rules
| `(arbitrary! Empty) => throwUnsupported
(0, 0)
#eval arbitrary! (Nat × Nat)
さらに、いずれかの規則が unsupportedSyntax
例外を投げた場合、そのコマンド内のそれ以降の規則はチェックされません。
macro_rules
| `(arbitrary! Nat) => throwUnsupported
redundant alternative #2
| `(arbitrary! Nat) => `(42)
macro_rules
| `(arbitrary! Int) => `(42)
macro_rules
| `(arbitrary! Int) => throwUnsupported
Nat
のケースはマクロ展開が arbitrary! : term
arbitrary!
構文をエラボレータにサポートされるものに変換しないため、エラボレートに失敗します。
#eval elaboration function for 'arbitrary!' has not been implemented
arbitrary! Nat
arbitrary! Nat
elaboration function for 'arbitrary!' has not been implemented
arbitrary! Nat
Int
のケースでは、2番目の例外を投げるものが試行された後に1番目のマクロ規則が試行されるため成功します。
42
#eval arbitrary! Int
14.5.5.2. macro
コマンド(The macro
Command)🔗
Lean.Parser.Command.macro : command
macro
コマンドは新しい 構文規則 の定義とそれを マクロ に関連付けることを同時に行います。 Lean.Parser.Command.notation : command
notation
コマンドが新しい項の構文のみを定義でき、展開がパラメータを代入する項であるのとは異なり、 Lean.Parser.Command.macro : command
macro
コマンドは任意の 構文カテゴリ の構文を定義でき、展開の生成に MacroM
モナドの任意のコードを使用できます。マクロは記法よりはるかに柔軟であるため、Lean は自動的に unexpander を生成することができません;つまり、 Lean.Parser.Command.macro : command
macro
コマンドで実装された新しい構文は、Lean への 入力 で使用できますが、Lean の出力で用いるにはさらなる作業を行わないといけません。
syntax
command ::= ...
| A `docComment` parses a "documentation comment" like `/-- foo -/`. This is not treated like
a regular comment (that is, as whitespace); it is parsed and forms part of the syntax tree structure.
A `docComment` node contains a `/--` atom and then the remainder of the comment, `foo -/` in this
example. Use `TSyntax.getDocString` to extract the body text from a doc string syntax node.
docComment?
(@[attrInstance,*])?
`attrKind` matches `("scoped" <|> "local")?`, used before an attribute like `@[local simp]`.
attrKind macro(:prec)? ((name := ident))? ((priority := prio))? macroArg* : ident =>
macroRhs
syntax
マクロの引数は構文アイテム( Lean.Parser.Command.syntax : command
syntax
コマンドで使われるもの)か、名前が付加された構文アイテムです。
macroArg ::=
stx
macroArg ::= ...
| ident:stx
展開では、構文アイテムに付けられる名前は束縛されます;これは適切な構文種別に対して TSyntax
型を持ちます。パーサによってマッチされた構文が定義された種別を持たない場合(例えば名前が複雑な仕様に適用されるためなど)、型は TSyntax Name.anonymous
となります。
ドキュメントコメントは新しい構文に関連付けられ、属性の種類(無し・local
・scoped
)は記法の場合と同じようにマクロの可視性を制御します:scoped
なマクロは定義されている名前空間か、その名前空間を開いている セクションスコープ で、local
マクロはローカルのセクションスコープでのみ利用可能です。
裏側では、 Lean.Parser.Command.macro : command
macro
コマンドは、 Lean.Parser.Command.syntax : command
syntax
コマンドと Lean.Parser.Command.macro_rules : command
macro_rules
コマンドに展開するようなマクロとして実装されています。マクロコマンドに展開される属性は構文定義に適用されますが、 Lean.Parser.Command.macro_rules : command
macro_rules
コマンドには適用されません。
14.5.5.3. マクロ属性(The Macro Attribute)🔗
マクロ は Lean.Parser.Attr.macro : attr
macro
属性を使用して構文種別に手動で追加を行うことができます。マクロを指定するこの低レベルな手段は、それ自身がマクロ定義を生成するマクロによるコード生成の結果である場合を除いて通常は有用ではありません。
attribute
Lean.Parser.Attr.macro : attr
macro
属性は指定された構文種別に対して マクロ と見なされる関数を指定します。
attr ::= ...
| macro ident
マクロ属性
syntax (name := rep) "[" num " !!! " term "]" : term
@[macro rep]
def expandRep : Macro
| `([ $n:num !!! $e:term]) =>
let e' := Array.mkArray n.getNat e
`([$e',*])
| _ =>
throwUnsupported
この新しい式を評価すると、マクロが存在することがわかります。
["hello", "hello", "hello"]
#eval [3 !!! "hello"]