Nimについて (III)

はじめて

能力が大きければ大きいほど、責任も大きくなります。

この記事は、Nimマクロシステムに関するチュートリアルです。マクロは、コンパイル時に実行され、Nim構文ツリーを別のツリーに変換する関数です。

マクロで実現できる関数の実例:

  • アサーションマクロです。アサーションが失敗した場合、比較演算子の両側に数値を出力します。myAssert(a == b)if a != b: quit($a " != " $b)に変換されます。
  • シンボルの値と名前を出力するデバッグマクロです。myDebugEcho(a)echo "a: ", aに変換されます。
  • 式の記号の違いです。diff(a*pow(x,3) + b*pow(x,2) + c*x + d, x) 3*a*pow(x,2) + 2*b*x + cに変換されます。(ax^3+bx^2+cx+dの微分の結果は3ax^2+2bx+cです。)

マクロのパラメータ

マクロの実際のパラメータには二義性があります。一方はオーバーロードの解析に使用され、もう一方はマクロ本体で使用されます。例えば、式foo(x)macro foo(arg: int)が呼び出された場合、xは整数と互換性のある型である必要がありますが、マクロ本体のargの型はintではなく、NimNodeです。具体的な実例を見れば、その理由は分かれます。

実際のパラメータをマクロに渡すには、2つの方法があります。実際のパラメータは、typedまたはuntypedのいずれかである必要があります。

型なし(untyped)パラメータ

型なしマクロの実際のパラメータは、セマンティックチェックの前にマクロに渡されます。これは、マクロに渡される構文木Nimを理解する必要がないことを意味します。唯一の制限は、解析可能にすることが必要です。通常、マクロは実際のパラメータをチェックしませんが、変換結果でそれを使用します。コンパイラはマクロ展開の結果をチェックするため、いくつかのエラーメッセージを除いて悪いことは起こりません。

型なしの実際のパラメータの欠点は、オーバーロードの解析に適していないことです。

型なしの実際のパラメータの利点は、構文木が予測可能であり、型付きよりも簡単です。

型付き(typed)パラメータ

型付きの実際のパラメータについては、セマンティックチェッカーはそれをマクロに渡す前にチェックして変換します。ここで、識別子ノードはシンボルに解決され、木での暗黙的な型変換は呼び出しと見なされ、テンプレートが展開されます。最も重要なことは、ノードに型情報があることです。型付きパラメータの実際のパラメータリストには、型付き型を指定できます。 ただし、intfloatMyObjectTypeなどのその他のすべての型も型付きパラメータであり、構文木としてマクロに渡されます。

静的パラメータ

静的パラメータは、構文木ではなくマクロに値を渡す方法です。例えば、macro foo(arg: static[int])に対しては、foo(x)式のxは整数定数である必要がありますが、マクロ本体ではargは単なる通常のint型です。

import macros

macro myMacro(arg: static[int]): untyped =
  echo arg # ただのint (7)であり、 ``NimNode``ではない

myMacro(1 + 2 * 3)

コードブロックの実際のパラメータ

呼び出し式の最後のパラメータは、インデント付きの独自のコードブロックで渡すことができます。例えば、以下のコード実例は、echoメソッドを呼び出すのに有効です(推奨されません)。

echo "Hello ":
  let a = "Wor"
  let b = "ld!"
  a & b

このような呼び出しはマクロに役立ちます。このマークアップを使用して、任意の複雑の構文木をマクロに渡すことができます。

構文木

Nim構文木を構築するには、構文木を使用してNimソースコードを表す方法と、Nimコンパイラが理解したときに木がどのように見えるかを了解する必要があります。Nim構文木ノードはmacrosモジュールに記録されます。 Nim構文木を学習するためのよりインタラクティブな方法は、macros.treeReprを使用することです。これは、構文木を複数行の文字列に変換し、コンソールに出力します。また、実際のパラメータ式が木の形式でどのように表されるか、および生成された構文木のデバッグと出力を調べるためにも使用できます。dumpTreeは、実際のパラメータを木の形式で出力する事前定義されたマクロです。木の表現の実例:

dumpTree:
  var mt: MyType = MyType(a:123.456, b:"abcdef")

# 出力:
#   StmtList
#     VarSection
#       IdentDefs
#         Ident "mt"
#         Ident "MyType"
#         ObjConstr
#           Ident "MyType"
#           ExprColonExpr
#             Ident "a"
#             FloatLit 123.456
#           ExprColonExpr
#             Ident "b"
#             StrLit "abcdef"

カスタムセマンティックチェック

マクロが最初に実際のパラメータが正しい形式であるかどうかを確認することです。すべての型のエラー入力をここでキャッチする必要はありませんが、マクロの評価中にクラッシュを引き起こす可能性のあるものはすべてキャッチして、適切なエラーメッセージを作成する必要があります。macros.expectKindmacros.expectLenは良いスタートです。 検査をより複雑にする必要がある場合は、macros.errorプロシージャを使用して任意のエラーメッセージを作成できます。

macro myAssert(arg: untyped): untyped =
  arg.expectKind nnkInfix

コードを生成する

コードを生成する方法は2つあります。newTreenewLitへの複数の呼び出しを含む式を使用して構文木を作成するか、quote do:式を使用します。1番目の方法は構文木生成に最適な低レベルの制御を提供し、2番目の方法ははるかに短いです。newTreenewLitを使用して構文木を作成することを選択した場合、marcos.dumpAstGenマクロが大いに役立ちます。quote do:は生成するコードを直接記述できます。逆引用符は、NimNodeシンボルから生成された式にコードを挿入するために使用されます。つまり、quote do: でバックティックを使用することはできません。シンボルを挿入する以外のことはできません。 生成された構文ツリーには、必ずNimNode型のシンボルのみを挿入してください。 newLitを使用して、任意の値をNimNode式の木型に変換し、木に挿入できます。

import macros

type
  MyType = object
    a: float
    b: string

macro myMacro(arg: untyped): untyped =
  var mt: MyType = MyType(a:123.456, b:"abcdef")
  
  # ...
  
  let mtLit = newLit(mt)
  
  result = quote do:
    echo `arg`
    echo `mtLit`

myMacro("Hallo")

「myMacro」を呼び出すと、以下のコードが生成されます:

echo "Hallo"
echo MyType(a: 123.456'f64, b: "abcdef")

最初のマクロを作成する

マクロの作成を開始するために、上記のmyDebugマクロを実装する方法を示します。まず、マクロの使用の実例を作成してから、実際のパラメータを出力します。これは、正しい実際のパラメータの様子が分かれます。

import macros

macro myAssert(arg: untyped): untyped =
  echo arg.treeRepr

let a = 1
let b = 2

myAssert(a != b)
Infix
  Ident "!="
  Ident "a"
  Ident "b"

出力から、実際のパラメータ情報は中置演算子(ノード型は「Infix」)であり、2つのオペランドはインデックス1と2にあることがわかります。この情報を使用して、実際のマクロを記述できます。

import macros

macro myAssert(arg: untyped): untyped =
  # すべてのノード型識別子の前には「nnk」が付いている
  arg.expectKind nnkInfix
  arg.expectLen 3
  # 演算子は文字列リテラルとする
  let op  = newLit(" " & arg[0].repr & " ")
  let lhs = arg[1]
  let rhs = arg[2]
  
  result = quote do:
    if not `arg`:
      raise newException(AssertionError,$`lhs` & `op` & $`rhs`)

let a = 1
let b = 2

myAssert(a != b)
myAssert(a == b)

これは生成されるコードです。マクロの最後の行にあるechoresult.reprステートメントを使用して、生成されたマクロをデバッグできます。それは、この出力を取得するために使用されるステートメントです。

if not (a != b):
  raise newException(AssertionError, $a & " != " & $b)

能力と責任

マクロは非常に強力です。マクロは式のセマンティクスを変更する可能性があるため、マクロの機能を理解しない人にとっては難しいです。テンプレートまたはジェネリックによって実装されたものと同じロジックを使用できます。マクロは使用しないことをお勧めします。マクロを特定の目的で使用する場合は、優れたドキュメントが必要です。自分が書いたコードが一目で明確できると言う人は、マクロを実装するのに十分なドキュメントが必要です。

制限

マクロはNim仮想マシンのコンパイラによって評価されるため、Nim仮想マシンのすべての制限があります。純粋なNimコードで実装する必要があります。マクロはshellで外部プロセスを開くことができ、組み込みコンパイラ以外のC関数を呼び出すことはできません。

その他の実例

この記事では、マクロシステムの基本について説明します。マクロで何ができるかについてのインスピレーションを与えることができるいくつかのマクロがあります。

Strformat

Nim標準ライブラリでは、strformatライブラリは、コンパイル時に文字列リテラルを解析するためのマクロを提供します。通常、このようなマクロで文字列を解析することはお勧めしません。解析されたASTは型情報を持つことができず、VMに実装された解析は通常それほど高速ではありません。ほとんどの場合、ASTノードでの操作が推奨される方法です。ただし、strformatは依然としてマクロの実際のアプリケーションの良い実例であり、assertマクロよりも少し複雑です。

Strformat

抽象構文木パターンマッチング(Ast Pattern Matching)

Ast Pattern Matchingは、複雑なマクロの作成に役立つマクロライブラリです。これは、新しいセマンティクスでNim構文木を再利用する方法の良い実例と見なすことができます。

Ast Pattern Matching

OpenGLサンドボックス

このプロジェクトには、GLSLコンパイラに発信する完全にマクロで記述されたNimがあります。クロスライブラリ関数をGPUで実行できるように、使用されているすべての関数シンボルを再帰的にスキャンしてコンパイルします。

OpenGL Sandbox

Share

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です