SATySFi のマクロ入門
この記事について
SATySFi のマクロは,インラインコマンドやブロックコマンドの形で多段階計算によるプリプロセス機構を利用できるようにするものである. SATySFi上でDSLを実装してパッケージなどとして提供するような場合には,最終的にマクロによるインターフェースとして提供するのが基本になるだろう.
この記事では,マクロの定義方法やエラー位置の取得方法について説明する.
前回の記事で,SATySFiの多段階計算について基本的な内容をまとめた.多段階計算でわからない部分があれば合わせて読んでみてほしい.
大半はSATySFi本体のソースコードや手元での実験に基づいて書いている. 間違いがあれば遠慮なく指摘してほしい.
バージョンは0.0.7
に基づいている.
マクロの定義
マクロの定義は,次の文法によって行う.
let-inline \macro-name@ param... = ... let-block +macro-name@ param... = ...
macro-name
はマクロの識別子であり,通常の変数名と同じである.
識別子の最後に@
文字をつけることに注意する.
param...
は,~param-name
またはparam-name
の並びである.
各パラメータは,ステージ0の変数として扱われる.また,
~param-name
(early macro parameter) は'a
型の変数param-name
(late macro paramter) は&'a
型の変数
として扱われる.
マクロの定義本体はステージ1のプログラムとして記述する.
返り値の型はインラインコマンドであればinline-text
,ブロックコマンドであればblock-text
になる必要がある.
通常のインラインコマンドなどと違い,コンテキストの第0引数を受け取る形の定義
let-inline ctx \macro-name@ param... = ...
はサポートされていないことに注意しておく.
また,module
の内部にマクロを定義することも現状できない.
マクロの適用
マクロの適用は,次の文法によって行う.
\macro-name@ arg...; +macro-name@ arg...;
通常のインラインコマンド,ブロックコマンドと同様,\macro-name@
はインラインテキスト中に,+macro-name@
はブロックテキスト中に書く必要がある.
arg...
は~(exp)
または(exp)
の並びである.括弧は省略できない.
exp
には通常の関数に渡す引数のとほとんど同じ形式の式を書ける.
パラメータの種類ごとに引数の渡し方に違いがある.
~param-name
の形のパラメータ (early macro parameter)~(exp)
の形で引数を渡す.param-name
にステージ0の式exp
が束縛される.
param-name
の形のパラメータ (late macro parameter)(exp)
の形で引数を渡す.param-name
にステージ0で&(exp)
と書いた場合に相当する値が束縛される.
マクロの適用は文書全体の評価よりも先に行われ,評価結果のインラインテキストに置き換えられる. マクロの評価の詳細については後で再び説明する.
簡単な例
マクロの使い方の確認のため,次のような例を考える.
@require: stdjareport let-inline \foo@ ~a b = let x = ~(lift-int a) + 1 in let y = ~b + 1 in let it = embed-string ((arabic x) ^ #` `# ^ (arabic y)) in it in document (| title = {Test of macro definition}; author = {sankantsu}; |) '< +p { \foo@ ~(100) (200); } >
例ではa
はint
型,b
は&int
型の変数になっている.
動作自体は受け取った引数それぞれに1を足して出力するだけの簡単なものであるが,~
をつけて宣言するかどうかで変数の使い方が違うのがわかる.
マクロの評価
先程の例で用いたマクロは,マクロを使わずに次のように書くのとほぼ等価である.
ただし,関数の定義部分は@stage: 0
の別ファイルに分けた.
% header0.satyh @stage: 0 let foo a b = &(let x = ~(lift-int a) + 1 in let y = ~b + 1 in let it = embed-string ((arabic x) ^ #` `# ^ (arabic y)) in it )
@require: stdjareport @import: header0 let-inline \id it = it in document (| title = {Test of macro definition}; author = {sankantsu}; |) '< +p { \id(~(foo 100 &200)); } >
次の点に注意しておく.
- マクロの定義はステージ0の関数定義になっている.また,定義全体を
&(...)
で囲んでいる. - マクロの適用はステージ0の関数適用になっている.
- 引数の受け渡しについて,
~(exp)
のタイプは単にexp
に,(exp)
のタイプは&(exp)
に置き換わっている.
- 引数の受け渡しについて,
マクロの動作がわかりにくいという場合は上のような置き換えを考えてみると良いかもしれない.
エラー位置の取得
マクロの導入のモチベーションは,DSLのパースをプリプロセス中に行うことで迅速なエラー報告を可能にするというものであった. これまで説明した内容だけでも,DSLの実装やエラー報告自体は可能になるが,ソースコード上のエラー位置を報告することができない. DSLである程度複雑な文法を扱うようにするとなると,入力箇所のどこにエラーがあるかわかったほうが便利だろう.
この問題を解決するために導入されたのが,@`...`
という形で記述する positioned literal というものである.
この形式で書かれた文字列は,通常の文字列string
に加えてコード中の位置を表すinput-position
型が付加されたinput-position * string
型として扱われる.
input-position
型の値は,
val get-input-position : input-position -> string * int * int
というプリミティブを用いることで文字列先頭位置の(ファイル名,列番号,行番号)
を取り出すことができる.
文字列位置の取得を行う簡単な例を示す (report-position.saty
という名前で保存されているとする.).
% report-position.saty @require: stdjareport let-inline \report-position@ ~posmsg = ~( let (ipos,msg) = posmsg in let (fname,line,col) = get-input-position ipos in &( let it-msg = embed-string ~(lift-string msg) in let it-fname = embed-string ~(lift-string fname) in let it-line = embed-string (arabic ~(lift-int line)) in let it-col = embed-string (arabic ~(lift-int col)) in {#it-msg; (at #it-fname;:#it-line;:#it-col;)} ) ) in document (| title = {Test of reporting position}; author = {sankantsu}; |) '< +p { \report-position@ ~(@`Hello!`); } >
コンパイルすると,本文としてHello! (at report-position.saty:24:27)
という文字列が得られ,文字列の先頭位置が取得できていることがわかる.
文字列の途中の位置を取得したい場合には,文字列をパースしていくときに同時に文字列中での現在位置を追っていく必要がある.
自分自身はまだ詳しく見れていないが,satysfi-base/parser
などで一応このあたりの基本的な機能は提供されているようである.
使用例: 2進文字列の10進変換
単純な例であるが,文字列で表現された2進数を10進変換して表示するマクロを作成した.
2進数表現の文字列を受け取り10進表示するマクロ\decode@
を提供する.
例えば,\decode@ ~(@`001011`);
とすれば,11
が表示できる.
\decode@ ~(@`0010a1`);
のように,0
,1
以外の文字を含めた場合には次のような表示をして停止する.
... preprocessing 'decode-binary.satyh' ... preprocessing 'stdjareport.satyh' ... preprocessing 'test.saty' ... ! [Error during Evaluation] test.saty:21:31: error of decode-binary: invalid character 'a'
出力されるログから,プリプロセス中にエラー検出がなされていることが確認できる.
実装と使用例を次に示す.
% decode-binary0.satyh @stage: 0 @require: list @require: base/char @require: base/string module DecodeBinary0 : sig val decode : input-position * string -> int end = struct type digit-char = | Digit of int | UnknownCharacter of Char.t type result-type = | DecodeVal of int | ErrorPos of int * Char.t let decode-char ch = let char-0 = Char.make `0` in let char-1 = Char.make `1` in if Char.equal ch char-0 then Digit(0) else if Char.equal ch char-1 then Digit(1) else UnknownCharacter(ch) let decode-str str = let char-lst = String.to-list str in let-rec aux (acc,pos) lst = match lst with | [] -> DecodeVal(acc) | ch :: tail -> ( match decode-char ch with | Digit(b) -> let accnew = 2*acc + b in aux (accnew, pos + 1) tail | UnknownCharacter(ch) -> ErrorPos(pos,ch)) in aux (0,0) char-lst let decode (ipos,str) = let (fname,line,col) = get-input-position ipos in let res = decode-str str in match res with | DecodeVal(value) -> value | ErrorPos(pos,ch) -> let error-msg = fname ^ `:` ^ (arabic line) ^ `:` ^ (arabic (col + pos + 1)) ^ `: `# ^ `error of decode-binary: invalid character` ^ #` '` ^ (Char.to-string ch) ^ `'` in abort-with-message error-msg end
% decode-binary.satyh @stage: 1 @import: decode-binary0 let-inline \decode@ ~posstr = let num-str = arabic (~(lift-int (DecodeBinary0.decode posstr))) in embed-string num-str
% test.saty @require: stdjareport @import: decode-binary let num-iter = 3000 let text = {The quick brown fox jumps over the lazy dog.} let-rec repeat n x = if n <= 0 then [] else (x :: (repeat (n - 1) x)) let long-text = repeat num-iter text |> List.fold-left (fun lhs rhs -> {#lhs; #rhs;}) {} in document (| title = {Decode binary digits}; author = {sankantsu}; |) '< +p { #long-text; } +p { 001011: \decode@ ~(@`0010a1`); } >
実装に関してはそれほど凝ったことはしていないので,詳しくは説明しない.
基本的には文字列を前から読んで2進数の値を計算していき,0
,1
以外の文字が来たらエラーを出力して停止するだけである.
ソースコードはレポジトリにもまとめてある.
参考資料
マクロに関する資料は現状非常に少ない.
マクロでコード中の位置を取得・使用する機能 · gfngfn/SATySFi Wiki · GitHub
gfngfn 氏のエラー位置報告機能に関する紹介.
コード中の位置を取得してエラー報告できるDSLのパッケージを試しに創ってみた · gfngfn/SATySFi Wiki · GitHub
同じく gfngfn 氏によるやや複雑なDSLによるデモ. 本記事より複雑な例を見たい場合は実装を読んで見ると良いと思う.