テンプレートエンジンとバックトレース

最近 haml の別実装 を書いてみていて、コード生成部分でバックトレースのことを考える必要があることに途中で気付いて、haml や slim が生成するコード中の謎の改行の理由がわかった話。

Ruby でよく使われる HTML テンプレートエンジンとして hamlslim がある。 どちらのテンプレートエンジンも、大まかなしくみとしては、

  1. ソース言語から Ruby のコードを生成 (コンパイル)
  2. 生成した Ruby のコードを eval してメソッドに変換
  3. 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 にはそのようなディレクティブは無いので、コンパイルする際にがんばって行番号をあわせる必要がある。