小さな DSL では、#define
C プリプロセッサ ディレクティブと同様に、マクロ定義を解析しています (ここでは単純な例を示します)。
_def mymacro(a,b) = a + b / a
パーサーが次の呼び出しに遭遇したとき
c = mymacro(pow(10,2),3)
に展開されます
c = pow(10,2) + 3 / pow(10,2)
私の現在のアプローチは次のとおりです。
- パーサーを State モナドでラップする
- マクロ定義を解析するとき、それらの本体を未解析の状態で保存します(文字列として解析します)。
- マクロ呼び出しを解析するときは、状態で定義を見つけ、本文の引数を置き換え、呼び出しをこの本文に置き換えて、解析を再開します。
最後のステップのコード:
macrocallStmt
= do -- capture starting position and content of old input before macro call
oldInput <- getInput
oldPos <- getPosition
-- parse the call
ret <- identifier
symbolCS "="
i <- identifier
args <- parens $ commaSep anyExprStr
-- expand the macro call
us <- get
let inlinedCall = replaceMacroArgs i args ret us
-- set up new input with macro call expanded
remainder <- getInput
let newInput = T.append inlinedCall (T.cons '\n' remainder)
setPosition oldPos
setInput newInput
-- update the expanded input script
modify (updateExpandedInput oldInput newInput)
anyExprStr = fmap praShow expression <|> fmap praShow algexpr
このアプローチはきちんと仕事をします。ただし、いくつかの欠点があります。
複数回の解析
任意の有効な DSL 式をマクロ呼び出しの引数にすることができます。したがって、テキスト表現のみが必要ですが (マクロ本体で置き換える必要があります)、それらを解析してから再度文字列に変換する必要があります。次のカンマを探すだけではうまくいきません。次に、カスタマイズされた完全なマクロが解析されます。したがって、実際には、マクロ引数は 2 回解析されます (また、コストがかかる show-ed も行われます)。さらに、呼び出しごとに、(ほぼ同じ) 本体の新しい解析が必要になります。本文をメモリ内で解析しないでおく理由は、最大の柔軟性を可能にするためです。本文では、マクロ引数から DSL キーワードを構築することもできます。
エラー処理
展開された本文は未使用の入力の前に挿入される (呼び出しを置き換える) ため、最初と最後の入力はまったく異なる場合があります。解析エラーが発生した場合、展開されたエラーが発生した位置入力が可能です。ただし、エラーを処理するときは、拡張されていない元の入力しかありません。したがって、エラーの位置は一致しません。そのため、上記のコード スニペットでは、状態を使用して展開された入力を保存し、パーサーがエラーで終了したときに使用できるようにしています。これはうまく機能しますが、展開のたびに新しいテキスト配列 (入力ストリームはテキスト) がストリーム全体に割り当てられるため、非常にコストがかかることに気付きました。おそらく、展開された入力をテキストではなく文字列として保持する方が、この場合、つまり中間部分を置き換える必要がある場合に安くなるでしょうか?
この質問の理由は次のとおりです。
- 上記の2つの問題に関する提案/コメントをいただければ幸いです
- 誰もがより良いアプローチを提案できますか?