ruby-trunk-changes r34132 - r34140

今日はキーワード引数の実装が追加されました。仕様はまだ正式に決定したわけではなくて叩き台ということなのでどんなものかチェックしてみてください。キーワード引数についての簡単な解説も書きました。

shirosaki:r34132 2011-12-26 22:49:31 +0900

mswin 版の IO#read で読み込みサイズを指定した時に、一時的にバイナリモードに変更して読み込みし、そのために読み込みバッファを破棄してファイルポインタを戻す(r34043 の変更)のですが、そこで "\n" だけの改行コードがあった場合に seek する位置を戻し過ぎることがあったのを修正、また read() のエラー処理を強化(というか元はエラー処理されてなかった?)しています。 [ruby-core:41671] [Bug #5714]

mame:r34133 2011-12-26 23:19:52 +0900

ここからキーワード引数の追加のためのコミットが続きます。 [ruby-core:40290] [Feature #5474]
まずパーサ部分の lambda のブロックを構文解析して AST(抽象構文木)を作る時に、ブロックパラメータ部分を読んだ時に一旦 NODE を作っておいて、lambda のブロック本体をパースした時に改めてその NODE の内容(子NODE)を代入しているというやりかたになっていたのを、ブロックのパース後にまとめて NODE を生成するようにするというリファクタリングです。 CRuby の AST は三分木になっていて、それぞれ子を3つまで持てます。これまで NODE_LAMBDA は 1つ目と2つ目の子にノードの参照を持っていたのですが、1つ目の参照はこの古い方法で一時的に格納していただけのようで、新しい方式では1つ目の子は持たないようになっています(元々持っていたNODEは 2つ目のノードの子だったのでなくなったわけではない)。

mame:r34134 2011-12-26 23:19:58 +0900

これもキーワード引数追加の布石としてのリファクタリングです。メソッドやブロックの仮引数の情報は AST 上では NODE の三分木をそのまま駆使して持たせていましたが、struct rb_args_info という構造体を導入して NODE_ARGS からこの構造体へのポインタを持たせることで簡潔に管理し、拡張しやすくするようにしています。 利用する NODE が減ってパフォーマンスも良くなる(こともある)みたいです。
これで AST の構造がかなり変わるので AST を直接触ってなにかするようなことをしているライブラリなどは対応が必要になるかもしれませんね。

mame:r34135 2011-12-26 23:20:03 +0900

parser 部分(AST を作るところまで)のキーワード引数対応実装。struct rb_args_info にキーワード引数用のメンバ(kw_args, kw_rest_args)を追加して、parse.y のメソッド引数やブロックパラメータ部分のルールをキーワード引数を追加した組み合わせを追加。引数のルールは省略可能引数や rest 引数などの組み合わせをそれぞれルールとしているのでルールがどーんと増えています。それにしてもすごい組合せパターン数。Ruby の仮引数の仕様は複雑です。これでもまだキーワード引数は通常の引数、省略可能引数、rest 引数の後、ブロック引数(&つきのやつ)の前と順番が決まってるのでいいですが、キーワード引数も順番関係なく書きたいとか言いだすとすごいことになりそう。

mame:r34136 2011-12-26 23:20:09 +0900

続いてコンパイル(AST -> YARV 命令列)と実行部分のキーワード引数対応の実装です。新しい命令の追加はなくて、キーワード引数情報つきののメソッド/ブロック呼び出しの時にはキーワード引数用の Hash オブジェクトへ send 命令で key?, delete メソッドを呼んで引数の値を取り出したり、またはデフォルト値をローカル変数/ブロック変数にセットする命令を生成するようにしています。 Hash#key? や Hash#delete を再定義している場合などは思わぬところで呼ばれる可能性があるので注意が必要ですね。またキーワード引数つきの呼び出しのテストを追加しています。

さて簡単にキーワード引数の現在の実装の仕様を解説します。詳細はチケットや追加されたテストを観てください。

まず以下のような構文でキーワード引数を受け取るメソッドを定義できます。

def m(a, b: 1)
  [a, b]
end

b がキーワード引数です。呼び出す時も似たような構文でキーワード引数を指定できます。 b は省略されるとデフォルトの値 1 が代入されます。

m(0) # => [0, 1]
m(0, b:2) # => [0, 2]

Python みたいにキーワード引数の順序に意味があって通常の引数のように渡せたりはしません。逆に言うとキーワード引数の指定の順序はいれかわってもかまいません。

def m2(a, b: 1, c: 2)
  [a, b, c]
end

m2(0)             #=> [0, 1, 2]
m2(0, c: 3)       # => [0, 1, 3]
m2(0, c: 1, b: 2) # => [0, 2, 1]
m2(0, 1, 2)       # => wrong number of arguments (3 for 1) (ArgumentError)

また仮引数にないキーワードを渡すのもエラーになります。

def m3(a: 0)
  a
end

m3(a: 1, b: 2)   # => unknown keyword: b (ArgumentError)

ただし "**" を前置した「キーワードrest引数」なるものを仮引数にもつ場合は未知のキーワード引数も受け付けて、そこに Hash オブジェクトとして格納されます。

def m4(a: 0, **kw)
  [a, kw]
end

m4(a: 1)    #=> [1, {}]
m4(a: 1, b: 2, c: 3) #=> [1, {:b=>2, :c=>3}]

ざっとこのような感じです。キーワード引数の仕様についてはまだ議論が続いていて、今回コミットされたものも最終決定というわけではなく今後の叩き台とするためという位置付けだそうですので、興味のある人は trunk を使ってみてアイデアがあればチケットや ML でコメントしてみてください。

mame:r34137 2011-12-26 23:20:15 +0900

更に Method#parameters でキーワード引数(:key)とキーワードrest引数(:keyrest)の情報も返すようにしています。そのためキーワード rest 引数がない時だけキーワード引数の一覧を iseq->arg_keyword_table に格納していたのを常に格納しておくようにして、未知のキーワードのチェックの要否は別途持っておくようにしています。

mame:r34138 2011-12-26 23:20:20 +0900

キーワードrest引数がない時に未知のキーワード引数が渡された時の ArgumentError のメッセージに不要なキーワードのリストを表示するようにしています。
この keys の String はこの関数内で生成しているので RB_GC_GUARD() をどこかに入れておいたほうが良さそうですね。しかし rb_raise() は NORETURN として宣言されてるので後ろに入れても効かない可能性もあるので悩ましいです。

nobu:r34139 2011-12-27 21:17:36 +0900

rb_args_info::pre_arg_num と post_arg_nun を rb_iseq_t の型にそろえて int 用にしています。また一応格納するところで long -> int の変換でオーバーフローのチェックをしています。

svn:r34140 2011-12-27 21:17:42 +0900

version.h の日付更新。