目黒.vim #16 に参加して Vim script に const を実装するパッチを書いた
あいにくの雨でしたが,Meguro.vim #16 に参加しました.
「この雨の中 Vim のもくもく会に来た者たちだ.面構えが違う」 #megurovim
— ドッグ (@Linda_pp) June 15, 2019
目黒.vim は Vim に関したり関しなかったりする作業をするもくもく会で,今回は Vim に :const
を追加するパッチを書きました.
:const
コマンドの機能
Vim script では変数を :let
で定義・代入します.ですが,
- 新しい変数定義時も代入時も同じ
:let
を使う if
などで新しいスコープが作成されない(動的スコープ)
により,意図せず既存の変数を上書きしてしまうケースがあります.
" Define variable let i = 0 if some_condition " In heavily nested or big statements... let i = 1 " Unexpectedly using the same name variable endif echo i " => 1 (expected 0)
意図せず変数が上書きされないようにするためのコマンドとして :lockvar
があります.他のプラグインなどから勝手に上書きされないようにグローバル変数をロックするのに使われたりはしますが,ローカル変数に使うほどのお手軽さはありませんでした.
- 毎回ロックするのは面倒だし忘れる
- 余分に1コマンド実行するコスト(パースおよび解釈)がかかる
一方,JavaScript では ES2015 から const
が追加されました.これは JavaScript の変数定義をレキシカルスコープにし,参照を変更不能にする構文です.
const number = 42; number = 99; // TypeError: invalid assignment to const `number'
Vim script でも内部的に :lockvar
の実装を使えば,これと似たような実装ができると思い, 変更しない変数の定義に :let
の代替として使える :const
を実装しました.:let
の代わりに :const
を使うだけなので,別途 :lockvar
を使うのに比べ面倒ではありません.また,変数の定義と同時にロックするので別コマンドで実行するのに比べてコストが遥かに安いです(実際,ただフラグを立てるだけ).
" Define locked variable const i = 0 if some_condition let i = 1 " Error! `i` is locked endif echo i " => 0 (expected)
リストの分解など複雑な定義式も :let
と同様に使えます.
const [a, b, c] = [1, 2, 3] const [x; xs] = [1, 2, 3]
さらに,:const
は安全のため,既存の変数を上書きできないようになっています.もし既存の変数をロックしたい場合は従来どおり :lockvar
を使ってください.
let i = 1 const i = 2 " E995 cannot modify existing variable
:const
でロックする深さは 1 です(:lockvar 1
相当).なので例えばリストを定義した時,要素まではロックされません.
const l = [1] call add(l, 2) echo l " => [1, 2]
なぜ再帰的に全ての値をロックする :lockvar!
相当にしないのかというと,
let a = 1 let l = [a] lockvar! l " 再帰的にロックするので a もロックしてしまう let a = 2 " エラー!a はロックされているので変更できない
のように意図せず変数をロックしてしまう可能性があるからです.また,ネストが深い辞書やリストをロックすると深さに比例したコストがかかってしまいます.
その他,注意する点は
- その性質上,環境変数(
$FOO
)やオプション値(&filetype
)には:const
は使えません(E996
) :unlockvar
で無理やり:const
で定義した変数のロックを解除することが可能です- JavaScript の
const
はスコープがレキシカルですが,Vim script の:const
はスコープがダイナミックのままです - JavaScript 処理系はコードのパース時に
const
定義された変数への再代入を検知してエラーにできますが,Vim スクリプト処理系は素朴なインタープリタなので:const
で定義された変数が実際に書き換えられようとするまでエラーになりません
:const
コマンドの実装
diff はこちら. src/eval.c
に ex_const()
を定義し, src/ex_cmds.h
に追加したいコマンドを定義して ex_const
関数ポインタを登録しておくと,:const
で ex_const()
が呼ばれます.
ちなみに新しいコマンドを追加した時は make cmdidxs
でコマンド検索表である src/ex_cmdidxs.h
を再生成する必要があります.
:const
は変数をロックする以外は :let
と同じ挙動のため,実装を :let
と共有しています.定義した変数をロックするかどうかのフラグを :let
の実装関数に追加し,フラグが立っている場合は変数定義時に di_flags
の DI_FLAGS_LOCK
フラグと di_tv.v_lock
の VAR_LOCKED
フラグを立てるだけです.複合代入(+=
など)や環境変数への代入やオプション変数への代入のパスはフラグが立っているとエラーにします.
テストの作成方法は src/testdir/README.txt
に書いてありますが,src/testdir
内に test_const.vim
を作成し,src/testdir/Make_all.mak
をちょっといじるだけでかなり簡単に実装できます.test_const.vim
内に Test_*
な名前の関数を定義しておくと, make test_const
でテストが実行できます.
:const
コマンドのデバッグ
変数に値をセットする関数 set_var_lval()
で,一時的に読み先の文字に NUL をセットしてあとで復帰している部分にエラーチェックを追加した際に復帰処理を追加するのを忘れてエンバグしてしまいました(const [a] = ...
の解釈途中で Internal error).
Vim では標準出力はエディタが使っているので printf()
などで出力することは基本的にできません.デバッグ方法は src/README.md
に書いてあり,
- デバッグプリントを
ch_log()
で出しておき,デバッグのための Vim 起動時に:call ch_logfile('log.txt', 'w')
のようにしてファイルにデバッグログを吐く.タイミング問題を追う時などに便利 :packadd termdebug
し,:Termdebug
で Vim の中で Vim を起動してgdb
を使ってデバッグする.Mac でgdb
を使うには実行ファイルをコード署名する必要があり若干面倒ですが,大変便利です
vim/vim へのプルリク
ドキュメントを書いて,パッチを GitHub の vim/vim リポジトリにプルリクします.
新機能の実装の場合,取り込まれるかどうかは作者の Bram さん次第なのでどうかなと思ったのですが,サクッとマージしてもらえました(Vim では変更をパッチとして取り込むので,GitHub のマージ機能を使っていません).Bram さんは普段 JavaScript や TypeScript を書くらしく,同じことを考えたことがあったらしいです.Vim script の新機能を Bram さんに説明したいときは JavaScript を引き合いに出すと良いのかもしれません.
あまりに速くマージされたので,いくつか細かいミスに後から気付き追加のパッチを送りました.