nimlang について日本語で検索しても英語で検索してもあまり情報がないのであまりつかっている人はいないかもしれませんが、Emacs の nimlang 用のメジャーモードである nim-mode のインデントを smie.el を使って改善したので紹介します。あと SMIE についての使用感とか…
ただ、Emacs25.1 で追加予定の if-let 関数(subr-x.el の)を使っており、むりやり古いバージョンの Emacs で動かそうとした場合 24.4 までがテストが通る限界でした。なので 24.3 以下ではおそらく travis のテストでしか確認してませんがダメだと思います。
nim-mode をアップデートすれば自動的に使えるようになっています。(というか古い python.el から流用しているインデントのコードははぼなくなった) あと関係ないですが、nimsuggest という nim 用の ide 支援ツールを使うと eldoc で情報が取得できれば表示されるようになっているのでインストールしたほうがいいです。
以降は smie について書きます。nim の構文は例として書きますが、nim については特に言及しません。
smie.el とは
説明なしでここまできてしまいましたが、emacs24.3(たぶん)で導入されたインデント計算エンジンです。
キータの記事 が非常にわかりやすかったです。
ざっくりいうと BNF っぽい書き方でインデント計算をよしなにやってくれるライブラリです。(実際は言語用のインデント数をあわせたり、デバッグでそんなにすんなりいかないかもしれませんが)
smie.el を使うメリット
以下のようなインデントを簡単に処理できます。
例 1: condition の部分が長く折り返す場合
if true or false and true or false
true or false and true or false:
echo "long condition lines should be indented"
elif true or false and true or false
true or false and true or false:
echo "long condition lines should be indented"
else:
echo "foo"
上記のような場合、カーソルのあるポイントから後ろに戻って if を探して etc.とかやってたのが、
(defconst nim-mode-smie-grammar
(smie-prec2->grammar
(smie-merge-prec2s
(smie-bnf->prec2
'((id)
(stmts (stmt ";" stmt) (stmts))
(stmt (exp) (exp))
(exp (id) (exp) (exp "=" exp))
(conditions
("if" exp "elif" exp "else" ":")))
;; 演算子の順位付けのための設定
;; 以下は if/when/case がネストした場合でも
;; 最短の親要素を探すために必要でした。(elif や else で
;; インデント計算するとき)
'((nonassoc "if" "when" "case")
(assoc "of") (assoc "elif") (assoc "else")))))
のように書くだけだったりと劇的に簡単にインデントを計算ができるようになります。(細かいインデント調整は必要になりますが、、、))
例 2: 同じ予約語を使いまわしている場合でも…
proc complex() =
case condition:
of "with colon":
if true:
echo "foo"
else: echo "one line else"
of "second OF":
echo "hello"
of "one line OF": echo "hello"
else:
echo "case done"
when true:
echo "hello"
elif true:
echo "make sure 'elif' is working"
else: echo "one line else"
if true:
echo "after ELSE ended with single line"
nim では if 文 when 文とも elif と else を使い、さらに case 文でも elif と else を使うという実際実装するのが非常にめんどくさそうな構文を持っています。(python.el からかなり パクって 再利用していたので古い nim-mode でも特に苦労はなかったですが、)
このような場合、問題になる elif と else がどこに属しているかを探すのが結構めんどうでした。
それが smie を使うとパースしたときに特定のインデント設定を書くのが非常に楽です。
どう楽かというと smie では XXX-smie-rules というような関数を各言語ように作り、トークンに対してインデント計算を補助できます。
上記の例でいうと:
(defun nim-mode-smie-rules (kind token)
"Nim-mode ’ s indent rules.
See also ‘ smie-rules-function ’ about KIND and TOKEN."
(pcase (cons kind token)
(`(:after . "else")
(if-let ((name (smie-rule-parent-p "if" "when" "case")))
...))))
のような感じで親が何かで簡単に分岐できます。
…実際には上記の例では smie-rule-parent-p は名前しか返してくれないので、`smie-indent–parent`関数を使いました。(親のポイントと名前を返してくれます。)
ただ名前に–が入っているしもしかしたら使わないほうがよいかも… XXX-rules 関数の中で使う文には問題はなかったです。
余談ですが、nim の case 文は case のあとにコロンはオプショナルで付けば1段階インデントを深くします。(なければ case と同じインデントが of, elif,else で続く)
実装するときの注意とか
- BNF っぽい書き方をする smie の grammar 変数ですが、なれるまで結構苦労するので、一個ずつテストを作ったほうがいいかもしれません。一つの変更が広範囲に影響するので、、
- 上記の grammar 変数ですが、いい感じに書くと親と子の要素を highlight してくれるので成功しているかのだいたいの確認になります。
(正確にコード読んでないですが、ハイライトが成功しているなら smie-rules-parent 関数とかが正常に機能していました。)
このハイライトですが、グラマーが複雑になってくると動かなくなる場合があるので(カーソルが動かなくなる)、以下のようにデフォルトの show-paren の関数を set するのもいいかもしれません。
(setq-local show-paren-data-function #'show-paren--default)
smie.el は作者が Emacs のメンテナーの Stefan Monnier さんなので、edebug に対応してたりとなかなかソースみるのが面白い は edebug に対応しているので M-x smie-edebug で SPC でステップ実行できるので emacs 初心者にも結構やさしいです。
自分は message buffer にでれば十分だったので以下のような advice を書いて edebug は使いませんでしたが、
(defadvice nim-mode-smie-rules (around output-debug-message activate)
"Output debug information."
(message (format "Point: %d" (point)))
(message (format "- IN - Kind: ’%s ’ Token: ’%s ’"
(ad-get-arg 0) (ad-get-arg 1)))
ad-do-it
(message (format "- OUT - ’%s ’" ad-return-value)))
- キータの記事でも書かれてましたが、smie.el を使って書かれている ruby-mode, elixir-mode や tuareg-mode を見てみるのが参考になりました。
python っぽい文法であれば、nim-mode が参考になるかも…まだ足りない部分かなりあるとおもいますが、
- xxx-smie-rules 関数に関する memo
- kind が:list-intro の時は数値でなく t or nil を返す
- :elem - empty-line-token の時は token を返す (実装が black magic 的って作者が comment に書いてる…)
おわり
smie.el メジャーモードのインデント計算では非常に便利だったので、新たに言語のメジャーモードを書く方がいましたら、ぜひ使ってみてください。