Emacs の why-mode のためのあれこれ

これは、Theorem Proving Advent Calendar の11日目の記事になります。
Why の Emacs 上での開発環境について紹介します。
ちっとも証明してないですが、凄い方々の凄い記事の間の箸休めということで一つ。

why-mode

tmiya さんによる3日目の記事で紹介されてた Why3 には、Emacs 向けの開発環境になるwhy.el が付属しています。Emacs の load-path が指しているディレクトリにこれを置き、

   (require 'why-mode "why.el")

とすれば読み込めます。なお require の第二引数はオプションで、省略した場合には第一引数に .el を付けたファイル名を探すようです。
しかしこの why.el は、コメントを見るに2002年に作られたものらしく、現在の Why3 では使えない機能も一部あります。この記事では、why.el を前提に3つの拡張を提案します。

Emacs じゃないけれど

ところで、zsh 向けのコマンド引数補完用ファイルも付属しています。2箇所ほど whyml3 と書かれているところを why3ml に修正して適切な場所に置くことで使えます。各種オプションやその引数をガイドしてくれ、ヘルプとしても大変便利です。

コメント

Emacs のデフォルトのキーバインドでは M-; でコメントの挿入ができます。ところが why-mode にはこのコメントとしてどんな記号を入力してやるかという設定がありません。
以下でコメントを認識させてやることができます。

(defun why-comment-setting ()
  (make-local-variable 'comment-start)
  (setq comment-start "(* ")
  (make-local-variable 'comment-end)
  (setq comment-end " *)")
  (make-local-variable 'comment-multi-line)
  (setq comment-multi-line t)
  )
(add-hook 'why-mode-hook 'why-comment-setting)

なおせっかくの複数行コメントなのに、複数行の範囲をコメントアウトするときも一行づつコメントアウトされてしまいますが、仕様です :-)

flymake

Emacs の標準機能として、プログラムのエラー箇所を表示する flymake というものがあります。構文や型システムに慣れないうちは特に助かります。これを WhyML 向けに設定してやります。
flymake は処理系のエラーメッセージに対して、一つのエラーにつき一行のメッセージという仮定を置いています。一方 why3ml は複数行のエラーメッセージを出力することがあるため、そのままでは利用できません。
幸か不幸か、why3ml はエラーを発見したら直ちに停止するため、一度の変換で最大一つのエラーしか報告できません。そこで flymake がエラーメッセージを読み込む直前に一行に纏めてやります。

(defvar flymake-read-multiple-lines nil)
(make-variable-buffer-local 'flymake-read-multiple-lines)
(set-default 'flymake-read-multiple-lines nil)

(defun fold-lines (list)
  (if list
      (concat (car list) " " (fold-lines (cdr list)))
    ""))

(defadvice flymake-split-output (after flymake-output-split-multilines)
  (when flymake-read-multiple-lines
    (let ((lines (car ad-return-value))
          (residual (cadr ad-return-value)))
      (setq ad-return-value
            (list (list (fold-lines lines))
                  residual)))))
(ad-activate 'flymake-split-output)

flymake-read-multiple-lines を t にすると全てのエラーメッセージを一行に纏めるようになります。あとは why-mode でのこの変数の設定をしつつ他の言語と同じように設定してやります。

(defun flymake-why-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-file  (file-relative-name
                       temp-file
                       (file-name-directory buffer-file-name))))
    (list "why3ml" (append '("--type-only") (list local-file)))))
(defconst flymake-allowed-why-file-name-masks '("\\.mlw" flymake-why-init))

(defvar flymake-whyml-err-line-patterns '("^File \"\\(.+\\)\", line \\([0-9]+\\), characters \\([0-9]+-[0-9]+\\)\\(.*\\)" 1 2 3 4))

(defun flymake-whyml-start ()
  (interactive)
  (setq flymake-read-multiple-lines t)
  (defadvice flymake-post-syntax-check (before flymake-force-check-was-interrupted)
    (setq flymake-check-was-interrupted t))
  (ad-activate 'flymake-post-syntax-check)
  (add-to-list 'flymake-allowed-file-name-masks flymake-allowed-why-file-name-masks)
  (add-to-list 'flymake-err-line-patterns flymake-whyml-err-line-patterns)
  (flymake-mode t))

(add-hook 'why-mode-hook 'flymake-whyml-start)

図では Why3 付属のサンプルプログラムを変更し、 型エラーを起こさせています。赤くなっている行では int 型を引数に取る関数に int 型でない変数を適用しています。
またエラーメッセージが下部のミニバッファに表示されています。flymake でのエラーメッセージの表示方法はいくつか考案されていて、これはそのうちの一つです(標準の表示方法である flymake-display-err-menu-for-current-line はエラー箇所が見えなくなることがあるので、今回は使いませんでした)。

compile

端末に戻らず、Emacs から証明器を動かした方が便利な場合もあるかと思います。
compile コマンドは、外部コマンドを非同期に実行してくれます。

私は smart-compile を利用して、以下のように設定しています。

(add-to-list 'smart-compile-alist
 '("\\.mlw$" . "why3ml -P alt-ergo %f --timelimit 1"))

図は Why3 付属のサンプルプログラムを実行してみたものです。ファイル中の命題をそれぞれ証明した結果が出力されています。証明結果については色を付けられると良さそうですね。

まとめと今後の課題

Emacs 上での Why 開発環境として、コメント入力の定義、flymake、compile を紹介しました。

今後以下の点ができたら嬉しいなと考えています。

  • 個別の命題の証明

compile では全ての命題を一挙に証明していますが、Why は 指定した命題だけを証明器に投げることができます。Emacs 上で命題の一覧から選択して証明、現在カーソルのある命題を証明といったことができるとインクリメンタルな開発がしやすくなると思います。

  • メジャーモード

付属の why.el は色付けやインデントの挙動などがイマイチで、それらの点に改良の余地があります。