header

2016年1月13日水曜日

Emacs の smie.el で nimlang 用のメジャーモードのインデントを改善した

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 メジャーモードのインデント計算では非常に便利だったので、新たに言語のメジャーモードを書く方がいましたら、ぜひ使ってみてください。

2016年1月5日火曜日

Blogger に post するコマンドラインツールを作った

このブログの更新には googlecl というコマンドラインツール経由で emacs から記事を書いていたけれど、結構前から googlecl が oauth2 に対応してなくてブログが更新できなかった。自分がなんか作った途端マーフィーの法則でもっといいツールが出たりすぐ治るのではという恐れがあってずっと googlecl の github Issues が更新されるのを待つ日々が続いたが、更新されなそうなので、シンプルに google blogger にポストするだけのコマンドラインツールを作った。実装には勉強ついでに nimlang を使ってみた。 (自分用に Emacs から post するコードも入っているので興味がある人は後半の方もみてみてください。)

前準備

nim の実行環境がない人は、先に ここから nim をインストールしてください。あと、 nimble という nim 用のパッケージマネージャもあったほうが管理が楽です。

あと Google の Blogger 用の API 利用するのに Google Developper Console で登録する必要があります。 もはや記憶の彼方でどうやったかあまり思い出せないのですが、多分ググればいろいろ情報があるのでは、、、(適当)

とりあえず、client id と client secret というのが必要です。

インストール

準備ができたら、

git clone https://github.com/yuutayamada/nim-blogger
cd CLONED DIRECTORY
nimble make

で nimblogger という実行ファイルが作れます。

ホントは nimble install でインストールできるようにしたかったけど、-d:ssl というフラグをつけないとビルドできず install のときどうやって指定するのかよくわからなかったので、このようになってしまいました。

# たぶん Google Developper Console の API manager から
# みつけられるとおもいます
export GOOGLE_API_CLIENT_ID="developper console から client_id をコピペしてください"
export GOOGLE_API_CLIENT_SECRET="developper console から client_secret をコピペしてください"

のあと

nimblogger --help

で使い方がでるとおもいます。 (注意 Refresh token なんちゃらってでたらもう一回やると成功するかもしれません。)

Emacs から

自分用なので使いたい人は使ってというスタンスで、misc ディレクトリに org-mode のドキュメントから html に変換してポストするコードが入ってます。

使いたい人は、下のような設定をしたあと`nimblogger:post-article`関数で投稿できるとおもいます。

(load "path/to/misc/nimblogger")
(setq nimblogger:blog-name "blog の名前"
      ;; binary file へのパス
      nimblogger:command "path/to/nim-blogger/nimblogger")
(define-key org-mode-map (kbd "C-S-p") 'nimblogger:post-article)

おわりに

自分が必要な最低限はできたけど、Google の更新についてく自信がないいので、いいツールがあれば教えてください。

2015年12月30日水曜日

Nimlang の関数を Emacs から呼び出す(途中までしかできてない)

Syohex さんの Emacs に mruby を組み込んでみた 記事を参考に Emacs の dynamic module 機能(Emacs 25.1 から導入予定)で Nimlang の関数を Emacs から呼べるか試してみた。ここ においてありますが、結論からいうと Emacs から nim の関数呼び出しには成功したが、引数を読み込もうとすると これ に近いエラーがでてコンパイルに失敗する。というわけでタイトルの通り途中までしかできていません。

上記の Nim の github issues を見ると作者の方が問題のコードリライトしてるらしく待ってればそのうちなおるのではという感じなのでしばらく待ってみようと思います。

これだけだと、あれなのでこんな感じになったよという報告だけ(だれに?)

# `plugin_is_GPL_compatible' indicates that its code is
# released under the GPL or compatible license; Emacs will refuse to
# load modules that don't export such a symbol.
{.emit:"int plugin_is_GPL_compatible;".}
import strutils
import emacs_module as emacs
emacs.addFunc(Fmod_test_return_t, 0):
  env.intern(env, "t".cstring)
emacs.defuns("libsample", """
DEFUN ("mod-test-return-t", Fmod_test_return_t, 1, 1, NULL, NULL);
""")

emacs_module というのは emacs-module.h の nim 用の wrapper で nim の練習ついでに作ってみました。(env.intern しか動くの確認できてないんですが、、、) 上記の nim のコードは引数がなんであれ t を返すというものです。なんかこれだけエラー起きずにコンパイルできました。`env.intern`の部分から Fmod_test_return_t 関数の中身を記述しています。addFunc というのは nim の template で次のように展開されます。

proc Fmod_test_return_t*(env: ptr emacs_env, nargs: ptrdiff_t,
                         args: ptr array[0..0, emacs_value],
                         data: pointer): emacs_value {.exportc.} =
  env.intern(env, "t".cstring)

自分の理解が正しければ、Emacs の dynamic module で C の関数を読み出す場合は引数と戻り値はすべて同じようなので(型が決まってるので) template で省略するようにしてみました。

まぁ特にまとめはありませんが、時間をおいてまた挑戦してみようと思います。ありきたりな感想ですが Emacs の拡張が Emacs Lisp 以外できたのは、ちょっと感動しました。

Popular Posts

Blogger templates

Blogger news