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)
5

エラーのケースがトリガーされると、ユーザはエラーメッセージを受け取ります:

#eval '5' is not allowed herenotFive 5
'5' is not allowed here

構文の一部をエラボレートする前に、エラボレータはその 構文種別 にマクロが関連付けられているかどうかをチェックします。これらは順番に試行されます。マクロが成功し、異なる種類の構文を返す可能性がある場合、チェックが繰り返され、構文の一番外側がマクロでなくなるまでマクロが繰り返し展開されます。その後、エラボレーションやタクティクの実行が可能になります。構文の一番外側(通常は node )だけが展開されます。このマクロ展開の出力にはマクロの構文が含まれる場合があります。これらの入れ子になったマクロはエラボレータがそこに到達した時に順番に展開されます。

特に、Lean では3つの状況でマクロ展開が発生します:

  1. 項のエラボレーションの際において、 構文の項エラボレータ を呼び出す前にエラボレートされる構文の一番外側のマクロが展開されます。

  1. コマンドのエラボレーションの際において、 構文のコマンドエラボレータ を呼び出す前にエラボレートされる一番外側のマクロが展開されます。

  1. タクティクの実行の際において、 その構文をタクティクとして実行する前に エラボレートされる一番外側のマクロが展開されます。

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 全体で同じマクロ機構を使用することができ、マクロは エラボレータ よりもはるかに書きやすくなります。

🔗def
Lean.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.

🔗def
Lean.Macro.expandMacro? (stx : Lean.Syntax)
    : Lean.MacroM (Option Lean.Syntax)

expandMacro? stx returns some stxNew if stx is a macro, and stxNew is its expansion.

🔗def
Lean.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 例外は、マクロ展開時の制御フローに使用されます。これは現在のマクロが受け取った構文を展開できないが、エラーが発生したわけではないことを示します。 throwErrorthrowErrorAt が投げる例外はマクロ展開を終了し、ユーザにエラーを報告します。

🔗def
Lean.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.

🔗def
Lean.Macro.throwError {α : Type} (msg : String)
    : Lean.MacroM α

Throw an error with the given message, using the ref for the location information.

🔗def
Lean.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 の処理は必要なスコープをすべて追加しますが、構文を直接構成するマクロは、導入する識別子にマクロスコープを追加しなければなりません。

🔗def
Lean.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.

🔗def
Lean.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)🔗

マクロの環境への問い合わせは限られた機能しかサポートされていません。定数が存在するかどうかをチェックし、名前を解決することはできますが、それ以上の考慮・吟味はできません。

🔗def
Lean.Macro.hasDecl (declName : Lean.Name)
    : Lean.MacroM Bool

Returns true if the environment contains a declaration with name declName

🔗def
Lean.Macro.getCurrNamespace
    : Lean.MacroM Lean.Name

Gets the current namespace given the position in the file.

🔗def
Lean.Macro.resolveNamespace (n : Lean.Name)
    : Lean.MacroM (List Lean.Name)

Resolves the given name to an overload list of namespaces.

🔗def
Lean.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🔗

QuotationSyntax 型のデータとして表現するためにコードをマークします。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 : Typecmd1 $application type mismatch cmd2.raw argument cmd2 has type TSyntax `command : Type but is expected to have type TSyntax `term : Typecmd2)

これにより、以下のような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 : identp:ident|Parse a p : identp here )

quotation は Syntax 型ではなく、 m Syntax 型を持ったモナドアクションです。 健全性の節 で説明されているように、 マクロスコープ と事前解決された識別子を追加することで hygiene を実装しているため、quotation はモナドです。使用する特定のモナドは quotation の暗黙のパラメータであり、 MonadQuotation 型クラスのインスタンスを持つ任意のモナドが適しています。 MonadQuotationMonadRef を継承しており、マクロ展開またはエラボレータが現在処理している構文のソース位置にアクセスすることができます。 MonadQuotation はさらに識別子に マクロスコープ を追加し、サブタスクに新しいマクロスコープを使用する機能を含んでいます。quotation をサポートするモナドは、 MacroMTermElabMCommandElabMTacticM です。

14.5.3.1. Quasiquotation🔗

Quasiquotationantiquotations を含むことができる 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
ex1 {m : Type → Type} [Monad m] [MonadQuotation m] (e : TSyntax `term) : m (TSyntax `term)

数値リテラルも有効な項であるため、$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
ex2 {m : Type → Type} [Monad m] [MonadQuotation m] (e : TSyntax `num) : m (TSyntax `term)

ドル記号と識別子の間にスペースを入れてはいけません。

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
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

この出力では、quotation は Lean.Parser.Term.do : termdo ブロックです。これは、処理中の現在のユーザ構文についてコンパイラに問い合わせることで得られる結果の構文のソース情報を構築することから始まります。次に、現在のマクロスコープと処理対象のモジュール名を取得します。これはマクロスコープはモジュールに対して追加されるため、独立したコンパイルが可能になり、グローバルカウンタが不要になるからです。次に、 Syntax.node1Syntax.node2 などの補助関数を使用してノードを構築します。これらの補助関数は指定された数の子を持つ Syntax.node を作成します。マクロスコープは各識別子に追加され、 TSyntax.raw は型付き構文ラッパーの内容を抽出するために使用されます。 xquote (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
ex1 {m : Type → Type} [Monad m] [MonadQuotation m] (xs : Syntax.TSepArray `term ",") : m (TSyntax `term)

しかし、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
ex2 {m : Type → Type} [Monad m] [MonadQuotation m] (xs : Array (TSyntax `term)) : m (TSyntax `term)

繰り返しの注釈は項の 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
#[0, 1, 2, 3]
カンマではない区切り文字

以下のリスト用の型破りな構文は、数値要素をカンマで区切るのではなく、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
[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 processede) : m Term := `(⟨| $(e)? |⟩) mkStx {m : Type → Type} [Monad m] [MonadQuotation m] (e : Option (TSyntax `term)) : m Term#check mkStx
mkStx {m : Type → Type} [Monad m] [MonadQuotation m] (e : Option (TSyntax `term)) : m Term

some を指定すると、オプショナルな項が存在することになります。

⟨| 5 |⟩#eval do logInfo ( mkStx (some (quote 5)))
⟨| 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 : termPattern 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. ifLean.Parser.Term.match : termPattern 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 : commandmacro_rules コマンドと Lean.Parser.Command.macro : commandmacro コマンドです。 Lean.Parser.Command.macro_rules : commandmacro_rules コマンドは既存の構文にマクロを関連付けますが、 Lean.Parser.Command.macro : commandmacro コマンドは新しい構文とそれを既存の構文に変換するマクロを同時に定義します。 Lean.Parser.Command.macro : commandmacro コマンドは Lean.Parser.Command.notation : commandnotation を一般化したものと見なすことができます。これは単純に置換するのではなく、プログラム的に生成された展開を許可するものです。

14.5.5.1. macro_rules コマンド(The macro_rules Command)🔗

syntax

Lean.Parser.Command.macro_rules : commandmacro_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 : identp:ident|)?Suitable syntax for p : identp ) => term)*

マクロのパターンは quotation パターンでなければなりません。これらはどの構文カテゴリの構文にもマッチしますが、1つのパターンがマッチするのは1つの構文種別だけです。quotation にカテゴリやパーサが指定されていない場合、それは項かコマンド(の列)にマッチしますが、両方にマッチすることはありません。あいまいな場合は、項のパーサが選択されます。

内部的には、マクロは各 構文種別 をそのマクロに対応付けるテーブルで追跡されます。 Lean.Parser.Command.macro_rules : commandmacro_rules コマンドは構文種別を明示的に注釈することができます。

構文種別が明示的に指定されている場合、マクロ定義は各 quotation パターンがその種別を持つかどうかをチェックします。quotation のパース結果が choice node であった場合(つまりパースが曖昧であった場合)、指定された種別を持つ各選択肢に対してパターンが1回複製されます。指定された種別を持つ選択肢が1つも無い場合はエラーとなります。

種別を明示的に指定しない場合、パーサが決定した種別が各パターンに使用されます。パターンがすべて同じ構文種別を持つ必要はありません;少なくとも1つのパターンで使用される構文種別ごとにマクロが定義されます。quotation パターンのパース結果が choice node であった場合(つまりパース結果が曖昧であった場合)はエラーとなります。

構文自体にドキュメントコメントが無い場合、 Lean.Parser.Command.macro_rules : commandmacro_rules に関連付けられたドキュメントコメントがユーザに表示されます。そうでない場合は、構文自体のドキュメントコメントが表示されます。

notationsoperators と同様に、マクロ規則も scopedlocal として宣言することができます。スコープ付きマクロは現在の名前空間が開いているときにのみ有効であり、ローカルなマクロ規則は現在の セクションスコープ においてのみ有効です。

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
none#eval addFirstThird [1]
none
some 4#eval addFirstThird [1,2,3,4]
some 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, 4)

名前空間が開かれると、マクロは 36 に置き換えます。

open ConfusingNumbers (6, 4)#eval (3, 4)
(6, 4)

数値リテラルやその他のリテラルの解釈をマクロで変更することは通常は有用ではありません。しかし、スコープ付きマクロは、 trivial のような拡張可能なタクティクに新しい規則を追加する時に非常に便利です。

裏側では、 Lean.Parser.Command.macro_rules : commandmacro_rules コマンドが quote パターンでマッチした構文種別ごとに1つのマクロ関数を生成します。この関数にはデフォルトケースとして unsupportedSyntax 例外を投げるケースを持つため、追加のマクロを試行することが可能です。

2つの規則を持つ単一の Lean.Parser.Command.macro_rules : commandmacro_rules コマンドは、必ずしも2つの単一ケースにマッチするコマンドと同じとは限りません。まず、 Lean.Parser.Command.macro_rules : commandmacro_rules の規則は上から下に試行されますが、直近で宣言されたマクロが最初に試行されるため、同じ挙動にするには順序を逆にする必要があります。さらに、マクロ内の以前の規則が unsupportedSyntax 例外を投げた場合、それ以降の規則は試されません;規則が別々の Lean.Parser.Command.macro_rules : commandmacro_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)
(42, 42)

すべてのマクロ規則が個別のケースとして定義されていた場合、上記は Nat の後に定義されたケースを使用したことでしょう。これは1つの Lean.Parser.Command.macro_rules : commandmacro_rules コマンドの規則は上から下にチェックされますが、より最近定義された Lean.Parser.Command.macro_rules : commandmacro_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)
(0, 0)

さらに、いずれかの規則が unsupportedSyntax 例外を投げた場合、そのコマンド内のそれ以降の規則はチェックされません。

macro_rules | `(arbitrary! Nat) => throwUnsupported redundant alternative #2| `(arbitrary! Nat) => `(42) macro_rules | `(arbitrary! Int) => `(42) macro_rules | `(arbitrary! Int) => throwUnsupported

Nat のケースはマクロ展開が arbitrary! : termarbitrary! 構文をエラボレータにサポートされるものに変換しないため、エラボレートに失敗します。

#eval elaboration function for 'arbitrary!' has not been implemented arbitrary! Natarbitrary! Nat
elaboration function for 'arbitrary!' has not been implemented
  arbitrary! Nat

Int のケースでは、2番目の例外を投げるものが試行された後に1番目のマクロ規則が試行されるため成功します。

42#eval arbitrary! Int
42

14.5.5.2. macro コマンド(The macro Command)🔗

Lean.Parser.Command.macro : commandmacro コマンドは新しい 構文規則 の定義とそれを マクロ に関連付けることを同時に行います。 Lean.Parser.Command.notation : commandnotation コマンドが新しい項の構文のみを定義でき、展開がパラメータを代入する項であるのとは異なり、 Lean.Parser.Command.macro : commandmacro コマンドは任意の 構文カテゴリ の構文を定義でき、展開の生成に MacroM モナドの任意のコードを使用できます。マクロは記法よりはるかに柔軟であるため、Lean は自動的に unexpander を生成することができません;つまり、 Lean.Parser.Command.macro : commandmacro コマンドで実装された新しい構文は、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 : commandsyntax コマンドで使われるもの)か、名前が付加された構文アイテムです。

macroArg ::=
    stx
macroArg ::= ...
    | ident:stx

展開では、構文アイテムに付けられる名前は束縛されます;これは適切な構文種別に対して TSyntax 型を持ちます。パーサによってマッチされた構文が定義された種別を持たない場合(例えば名前が複雑な仕様に適用されるためなど)、型は TSyntax Name.anonymous となります。

ドキュメントコメントは新しい構文に関連付けられ、属性の種類(無し・localscoped)は記法の場合と同じようにマクロの可視性を制御します:scoped なマクロは定義されている名前空間か、その名前空間を開いている セクションスコープ で、local マクロはローカルのセクションスコープでのみ利用可能です。

裏側では、 Lean.Parser.Command.macro : commandmacro コマンドは、 Lean.Parser.Command.syntax : commandsyntax コマンドと Lean.Parser.Command.macro_rules : commandmacro_rules コマンドに展開するようなマクロとして実装されています。マクロコマンドに展開される属性は構文定義に適用されますが、 Lean.Parser.Command.macro_rules : commandmacro_rules コマンドには適用されません。

14.5.5.3. マクロ属性(The Macro Attribute)🔗

マクロLean.Parser.Attr.macro : attrmacro 属性を使用して構文種別に手動で追加を行うことができます。マクロを指定するこの低レベルな手段は、それ自身がマクロ定義を生成するマクロによるコード生成の結果である場合を除いて通常は有用ではありません。

attribute

Lean.Parser.Attr.macro : attrmacro 属性は指定された構文種別に対して マクロ と見なされる関数を指定します。

attr ::= ...
    | macro ident
マクロ属性/-- Generate a list based on N syntactic copies of a term -/ 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"]
["hello", "hello", "hello"]