vimrcをVim9 scriptで書き換えてみて

ぺりー
14 min readNov 1, 2023
Vim9scriptation

本記事はVim駅伝の2023–11–01に寄稿しました。

こんにちは!Vim scriptの書き心地に慣れないぺりーです。

Vim9 scriptに移行したら速くなった!と聞いて、書き心地を確かめつつ、あわよくば速くなれば良いなと思いつつ、Vim9 scriptに移行してみました。

今回はVim9 scriptに最速で移行できるように、vimrcをVim9 scriptに移行して詰まったところを共有します

Vim9 scriptは開発中のため、移行時は注意してください
また、詳細はヘルプドキュメントを参照ください

目次

  1. 旧来のVim scriptとVim9 script
  2. Vim9 scriptの主な変更点のまとめ
  3. コンパイルを通すまでに必要な手順
  4. コンパイルエラーになった箇所
  5. Vim9 scriptで書き換えてみて

旧来のVim scriptとVim9 script

旧来のVim scriptはViと互換性を維持するため、古い仕様を変更できず、実行するたびに各行のパースが行われており、パフォーマンスが良いとは言えませんでした。

Vim9 scriptは下位互換性を一部捨てることで10倍から100倍実行速度を向上させました。

また旧来のVim script特有の文法ではなく、より一般的に使われる文法が使われるようになりました。

旧来のVim scriptとVim9 scriptは同時に利用することができるため、高速化したい関数から取り組むと良いと思います。(全て置き換えなくても問題なく動作します)

*私の場合はvimrcをまるっと全て書き換えたため、これから紹介する手順は全て置換する前提です。

Vim9 scriptの主な変更点のまとめ

  • コメントの開始が # に変更
  • 行継続文字( \ )がほとんどの場合、不要
  • 値の代入に let を使用できない
  • 変数の宣言には var を使用
  • final , const の使用
  • 関数内でネストして関数を定義できるように
  • 関数の宣言に def を使用
  • def で定義される関数はエラーが発生次第、実行を中断するように
  • 変数と関数のスコープが明示しない限りスクリプトローカルに
  • 関数の引数と戻り値に型が必要に
  • 関数の呼び出し時の :call , メソッド呼び出しの :evalが不要に
  • 範囲指定付きのコマンドはコロンの前置きが必須
  • 再読み込みで関数と変数がクリアされるように
  • ラムダ式では => を使用

などなど詳細はヘルプドキュメントを参照ください

コンパイルを通すまでに必要な手順

ここでは最低限コンパイルを通すまでに必要な手順を紹介します
そのため、 :call:eval の削除等は含んでいません。

  1. vim9script をvimrcの先頭行に記述する
  2. コメントを " から # に置換する
  3. func[tion]def に置換し abortを削除する
    ( endfunc[tion]enddef にする)
  4. 引数の型と戻り値の型を書く(一旦 any にする)
  5. letvar に置換する(代入の let は削除する)
  6. プレフィックス s: と引数辞書 a: を削除する
  7. 引数リストa:000list 型に置換する
  8. 文字列結合の ... に置換する
  9. スペースが必要な箇所にスペースを入れる
  10. defcompile を最終行に記述して :source % しコンパイルチェックを行う

1. vim9script をvimrcの先頭行に記述する

:0put ="vim9script"

行頭に必ず必要です

2. コメントを " から # に置換する

:%s/"/#/gce

コメント以外のダブルクォーテーションに注意しながら置換していきます

3. func[tion]def に置換し abortを削除する
( endfunc[tion]enddef にする)

:%s/\v\_^(function|func)(!)?\s*%(s:)?(g:)?(\w+)%(\s*)(\(.*\))/def\2 \3\u\4\5: any/ge |
%s/\v%(\s*)abort//ge |
%s/\v\_^end(function|func)/enddef/ge

スクリプトローカルの関数が大文字始まりになることも確認します
Vim9 scriptではエラーが発生次第中断するようになったため abort は禁止です

4. 引数の型と戻り値の型を書く(一旦 any にする)

:%s/\v(def!?\s)%(g:)?(\w+)\(%(a:)?(\w+)\)/\1\2(\3: any)/ge |
%s/\v(def!?\s)%(g:)?(\w+)\(%(a:)?(\w+)%(,\s*)%(a:)?(\w+)\)/\1\2(\3: any, \4: any)/ge |
%s/\v(def!?\s)%(g:)?(\w+)\(%(a:)?(\w+)%(,\s*)%(a:)?(\w+)%(,\s*)%(a:)?(\w+)\)/\1\2(\3: any, \4: any, \5: any)/ge

*上記のExコマンドは引数3つまでにしか対応していないので引数が4つ以上ある場合は拡張してください

引数と戻り値は必ず型宣言が必要ですが、分からない場合は any にしておけばコンパイルは通ります(実行時に型チェックされます)

- func MyFunc(text)
+ def MyFunc(text: string): any

型がわからないときは typename() を使用して型を調べます

5. letvar に置換する(代入の let は削除する)

:%s/\vlet\s*(b:|w:|g:|t:)/\1/ge | %s/\vlet\s*%(s:)?/var /ge

既に宣言されているローカル変数と b: w: g: t: 変数の代入に使用される let を削除する必要があります

*上記のExコマンドは既に宣言されているローカル変数に let を使用していた場合にも var に置き換えてしまうため、別途削除が必要です

- let lnum = 1
- let lnum += 3
- let b:result = 42
+ var lnum = 1
+ lnum += 3
+ b:result = 42

6. プレフィックス s: と引数辞書 a: を削除する

:%s/\v%(s:)(\w+)/\u\1/ge | %s/\va:(\w+)/\1/ge

プレフィックスのない関数と変数は全てスクリプトローカルになるため s: を使用することはできません
引数辞書 a: も使用できません

- return len(a:text)
+ return len(text)

7. 引数リストa:000list 型に置換する

4. 引数の型と戻り値の型を書く に示したExコマンドを使用した場合は引数が 000 に書き換わるため、コンパイルエラーが発生します
正規表現では直すのが難しいため、コンパイルエラーが発生したときに直します

引数リストも引数辞書同様に使用できません
最後の引数として名前と list 型で定義します

def MyFunc(...itemList: list<number>)

8. 文字列結合の ... に置換する

:%s/\s\.\s/ .. /gce

文字列の中に \s.\s を含んでいる場合に注意する
このExコマンドは . の前にスペースがあることを前提にしています

9. スペースが必要な箇所にスペースを入れる

:%s/\v(^[^#].*[^ ])#/\1 #/ge |
%s/\s*=\s*/ = /ge |
%s/\v\[%(\s*)(\w+)%(\s*)\:%(\s*)\]/[\1 :]/ge |
%s/\v\[%(\s*)\:%(\s*)(\w+)%(\s*)]/[:\1]/ge |
%s/\v\[\s*:\s*\]/[:]/ge |
:%s/\v\[\s*(\w+)\s*:\s*(\w+)\s*\]/[\1:\2]/ge

コマンドと # , コメント文の間にはスペースが必要

var name = value # コメント
var name = value# エラー!

= の前後にスペースが必要(大抵の演算子の周りで必須)

var name=234    # エラー!
var name= 234 # エラー!
var name =234 # エラー!
var name = 234 # OK

サブリストの : の周りにスペースが必要

otherlist = mylist[v : count] # v:count は異なる意味を持つ
otherlist = mylist[:]               # リストのコピーを作る
otherlist = mylist[v :]
otherlist = mylist[: v]

関数名と ( の間にスペースは禁止
3. func[tion]をdefに変換しabortを削除する でExコマンドを実行した場合は、改行がない場合のみスペースが取り除かれています

Func (arg) # エラー!
Func
\ (arg)       # エラー!
Func
(arg)       # エラー!
Func(arg)   # OK
Func(
arg)         # OK
Func(
arg           # OK
)

10. defcompile を最終行に記述して :source % しコンパイルチェックを行う

:$put ='defcompile'

defcompile を記述することでクラス内部で定義された関数以外をコンパイルすることができます。
一部の実行時エラーに実行前に気づくことができるので移行中は最終行に記述しておくのが良いと思います

コンパイルエラーになった箇所

おそらく先に示した手順だけではコンパイルエラーが発生すると思います
私が遭遇したエラーになるケースとその対応を取り上げてみます

E1267: Function name must start with a capital:

関数名はスクリプトローカルであっても大文字始まりにしなければなりません

E1052: Cannot declare an option: &t_SI =
E1016: Cannot declare a global variable:
E1016: Cannot declare an environment variable: $HOGE =

グローバル変数、Optionや環境変数はletが不要です

E1068: No white space allowed before ':':

dictionaryでkey, valueの間にスペースは禁止です

{key : value} # NG
{key: value}   # OK

E1050: Colon required before a range:

Exコマンドに範囲を指定する場合はコロンが必要です

- let prevbuf = bufnr('\[update plugins\]')
- if prevbuf != -1
- execute prevbuf . 'bwipeout!'
- endif
+ var prevbuf: number = bufnr('\[update plugins\]')
+ if prevbuf != -1
+ # Needs `:` before Ex command with range.
+ execute ':' .. prevbuf .. 'bwipeout!'
+ endif

E1012: Type mismatch; expected number but got string

map() に渡されたリストか辞書の要素の型は変更できません

var mylist: list<number> = [1, 2, 3] # 型推論させても同じエラーになるがanyを宣言すればエラーにならない.
- echo map(mylist, (i: number, _): string => 'item ' .. i)
+ echo mapnew(mylist, (i: number, _): string => 'item ' .. i)
+ # ['item 0', 'item 1', 'item 2']

E1091: Function is not compiled:

ユーザーコマンドが後で定義されている場合に発生します
先に定義するか、 execute('function') を使用して間接的にコマンドを呼び出すことでエラーを回避することができます

command -nargs=1 MyCommand echom <q-args> # 先に定義する
def Works()
MyCommand 123
enddef

def Works()
command -nargs=1 MyCommand echom <q-args>
MyCommand 123 # エラー
execute 'MyCommand 123' # 間接的に呼び出す
enddef

他にも number 同士を比較するのに 同一インスタンスかどうか検証するis を使っているためエラーが発生することもありました

Vim9 scriptで書き換えてみて

型付け、一般的なプログラミング言語の書き方、関数のスコープなど様々な変更によってとにかく書き心地と可読性が向上しました

またスペースなどフォーマットが揃うのも可読性に寄与しています

完全移行によりVim 9.0以降でないと動かなくなってしまったため、ポータビリティが下がってしまったのがデメリットです

こちらは仕方ないのですが、LSPのSyntaxが対応していないのもデメリットになるかもしれません

さて、もちろん起動時間が速くなると期待して置き換えたわけですが、有意な差が出ませんでした、残念!(そもそも重たい処理を行う関数がなかった)

実際のデータは下記のとおりです
全体では 1ms, vimrc単体だと0.5msほど速くなったみたいですね

$ vim-startuptime -script -count=1000

# Before
Total Average: 15.064100 msec
Total Max: 20.946000 msec
Total Min: 14.531000 msec

# After
Total Average: 14.783616 msec
Total Max: 18.224000 msec
Total Min: 14.130000 msec

AVERAGE MAX MIN
---------------------------
8.053200 11.622000 7.304000: $HOME/.vimrc # Before
7.694375 9.630000 7.338000: $HOME/.vimrc # After

参考にした記事など

--

--

ぺりー

Satoru Kitaguchi. Backend Engineer at eureka, Inc.