最近 haml の別実装 を書いてみていて、コード生成部分でバックトレースのことを考える必要があることに途中で気付いて、haml や slim が生成するコード中の謎の改行の理由がわかった話。
Ruby でよく使われる HTML テンプレートエンジンとして haml と slim がある。 どちらのテンプレートエンジンも、大まかなしくみとしては、
- ソース言語から Ruby のコードを生成 (コンパイル)
- 生成した Ruby のコードを eval してメソッドに変換
- render の際は、適切にインスタンス変数やローカル変数等を与えてメソッド呼び出し
という実装になっている。
haml も slim も、テンプレート中に (ほぼ) 任意の Ruby の式を埋め込むことができる。 すると、テンプレートの render 中に例外が発生することも当然ありうる。 このとき、バックトレースにはちゃんと例外の発生箇所が記録されていてほしい。
テンプレートエンジンのソース言語は Ruby ではないので、例外発生箇所を正しく記録するためには、Ruby の式が埋め込まれる箇所に関してはソース言語とコンパイル後のコードの間で行番号を一致させる必要がある。 たとえば、
%div %ul - @items.each do |x| %li= 'item: ' + x
という haml テンプレートに対して、haml 4.0.6 は次のようなコードを生成する。
_hamlout.buffer << "<div>\n<ul>\n"; @items.each do |x| _hamlout.buffer << "<li>#{_hamlout.format_script_false_true_false_true_false_true_true(('item: ' + x ));}</li>\n";end;_hamlout.buffer << "</ul>\n</div>\n";;_erbout
このように途中に謎の空行を挟みつつ、Ruby の式が埋め込まれている @items.each
と 'item: ' + x
に関してはソース言語と行番号が一致するようにコンパイルされている。
この対応のおかげで、主に開発中に例外が発生した際のバックトレースがテンプレート言語内であっても正確に表示できるようになっている。
C のコードを生成するようなツールだと #line
ディレクティブを使って行番号をあわせていることがあるけど、Ruby にはそのようなディレクティブは無いので、コンパイルする際にがんばって行番号をあわせる必要がある。