この記事はVim Advent Calendar 2014 - Qiita 24日目の記事です。
Matz さんが streem という、ストリーム指向言語の開発を始めるらしいです。
https://github.com/matz/streem
まだ文法の設計段階ではあるけど、それなのにかなりの量の pull-req がバンバンと来てて凄いなーと思いつつも「この pull-req 量だと僕には出番無いなー」と思ったのと「Matz さんがもしかしたら Go で streem を実装するかもしれない」という記事を読み「streem の他言語実装が一つ消えてしまう。これはまずい。」と思ったので、README.md に書かれているサンプルだけを頼りに streem を Vim script で実装してみました。
先日はネタで streem のマネをして golang で実装したりしましたが、本日ネタが無い中にどうしても Vim script で streem を実装したくなったのでやってみました。
どんなものか
とりあえずこんな事が出来ます。まず以下のバッファを用意します。
foo
bar
baz
そして以下のコマンドを実行します。
:%Streem {|x| x += " Matz" } | STDOUT
すると画面に
foo Matz
bar Matz
baz Matz
と表示されます。
どうやって動いているのか
まず、Streem
コマンドは Vim の range から行を入力として扱います。つまり上記で言えば ['foo', 'bar', 'baz']
となります。
そして引数のコマンドを解析し、AST(抽象構文木)に分解します。分解に使ったマッチパターンテーブルは以下の通り。
let s:tbl = [
\ ['stmts',
\ [
\ { 'type': 'node', 'match': ['stmt', ['lb', 'stmt']], 'eval': 's:stmts' },
\ ],
\ ],
\ ['stmt',
\ [
\ { 'type': 'node', 'match': ['expr', ['|', 'expr']], 'eval': 's:stmt' },
\ ],
\ ],
\ ['expr',
\ [
\ { 'type': 'node', 'match': ['expr_node', 'sp', '==', 'sp', 'expr_node'], 'eval': 's:op_eqeq' },
\ { 'type': 'node', 'match': ['if', 'sp', 'expr', 'sp', 'end'], 'eval': 's:expr_if' },
\ { 'type': 'node', 'match': ['ident', '(', ')'], 'eval': 's:op_call' },
\ { 'type': 'node', 'match': ['ident', '(', 'expr_node', ')'], 'eval': 's:op_call' },
\ { 'type': 'node', 'match': ['ident', 'sp', '=', 'sp', 'expr_node'], 'eval': 's:op_let' },
\ { 'type': 'node', 'match': ['ident', 'sp', '+=', 'sp', 'expr_node'], 'eval': 's:op_plus' },
\ { 'type': 'node', 'match': ['expr_node', ['sp', '|', 'sp', 'expr_node']], 'eval': 's:expr' },
\ { 'type': 'node', 'match': ['expr_node'], 'eval': 's:expr' },
\ ],
\ ],
\ ['expr_node',
\ [
\ { 'type': 'node', 'match': ['{', 'sp', '|', 'ident', '|', 'sp', '}'], 'eval': 's:expr_func' },
\ { 'type': 'node', 'match': ['{', 'sp', '|', 'ident', '|', 'sp', 'stmts', 'sp', '}'], 'eval': 's:expr_func' },
\ { 'type': 'node', 'match': ['ident'], 'eval': 's:expr' },
\ { 'type': 'node', 'match': ['number'], 'eval': 's:expr' },
\ { 'type': 'node', 'match': ['string'], 'eval': 's:expr' },
\ ],
\ ],
\ ['string', [{ 'type': 'regexp', 'match': '\("[^"]*"\|''[^'']*''\)', 'eval': 's:expr_string' }]],
\ ['number', [{ 'type': 'regexp', 'match': '[0-9]\+', 'eval': 's:expr_number' }]],
\ ['ident', [{ 'type': 'regexp', 'match': '[a-zA-Z][a-zA-Z0-9]*', 'eval': 's:expr_ident' }]],
\ ['+=', [{ 'type': 'string', 'match': '+=', 'eval': '' }]],
\ ['=', [{ 'type': 'string', 'match': '=', 'eval': '' }]],
\ ['|', [{ 'type': 'string', 'match': '|', 'eval': '' }]],
\ ['if', [{ 'type': 'string', 'match': 'if', 'eval': '' }]],
\ ['end', [{ 'type': 'string', 'match': 'end', 'eval': '' }]],
\ ['sp', [{ 'type': 'regexp', 'match': '[ \t]*', 'eval': '' }]],
\ ['lb', [{ 'type': 'regexp', 'match': '[ \t]*[\\r\n;]\+[ \t]*', 'eval': '' }]],
\ ['{', [{ 'type': 'string', 'match': '{', 'eval': '' }]],
\ ['}', [{ 'type': 'string', 'match': '}', 'eval': '' }]],
\]
これを YACC っぽくパースし、各ノードに分解します。例えば +=
オペレータであれば以下のノードになります。
{'type': 'op_plus', 'value': [{'type': 'ident', 'value': 'x'}, {'type': 'expr', 'value': [{'type': 'string', 'value': 'Matz'}]}]}
これを小さな VM (今回は時間が無くて streeem が実装している程の命令をサポート出来ませんでした)で実行します。STDOUT は入力を echo するだけのオブジェクトです。
また今回は残念ですが concurrency ではありません。メモリを使いシーケンシャルに実行しています。
ただ、これだけは覚えておいて下さい。
Vim に出来ないから実装しなかった訳ではない
やろうと思えば remote API を使い、複数立ち上げた vim と非同期通信を行いながら結果を集める事だって出来ます。
とここまで書きましたが、そろそろクドいし面倒臭くなってきたと思うので、どうやって動いているか知りたい人はソースを見てください。
mattn/streem-vim - GitHub
https://github.com/mattn/streem-vim
さいごに
さて、明日で Vim Advent Calendar 2014 が完走します(2日程抜けてしまいましたが)。皆さんお疲れ様でした。まさか開発されて20年近くにもなるテキストエディタでこれだけのブログ記事があがって来るとは誰が想像したでしょうか。
また今年も数多くの不具合報告が vim-jp へと寄せられ、数多くのバグが vim-jp によって修正されました。バグ報告頂いた皆さん、そしてパッチを書いてくれた vim-jp のメンバに感謝したいと思います。ありがとうございました。
来年もよい Vim 年になる事を祈って、僕の記事を終えさせて頂きます。
皆様、良いお年を。