Okinawa.rbのAdvent Calendar 2016 10日目です。
RubyでMethodオブジェクトを取り出すsyntaxが欲しいときってありますよね。ruby-coreでo.:method
やo[.method]
やo->method
などが提案されていますがしっくりこないみたいです。
https://bugs.ruby-lang.org/issues/12125#change-57209
Rubyのsyntaxをいじる練習に提案されているsyntaxとは別のsyntaxを雑に実装してみようと思います。
どういうsyntaxを実装するか
ここにオブジェクトがあります。
o = Object.new
オブジェクトのmethodメソッドの取り出し方を考えます。
とりだすメソッドの名前を指定しないと取り出せないので、とりあえずmethodと書く必要はあるでしょう。
o /*何かしらのsyntax*/ method
るりまのRubyで使われる記号の意味やリテラルのページを見ると記号はだいたい使われていそうです。
RubyではProcをつくるときに-> { }
、ブロックを作るときにfoo { }
のように{}
を使うことが多いですね。methodメソッドもProcやブロックと同じようにcallできるので同じように{}
を使って書きたいです。
なので、今回はreceiver.{method}
でMethodオブジェクトを取り出せるようにしてみましょう。
以降.{}
演算子と呼びます。
準備
% git clone git://github.com/ruby/ruby.git
% cd ruby
% git fetch --tags
% git checkout -b method_method_syntax v2_4_0_preview3
今回はsyntax雑に足してみるのが目的なのでビルドは各自READMEを読んでやっていき
https://github.com/ruby/ruby#how-to-compile-and-install
テスト
雑に足すだけなので以下でapplepenと表示されればいいことにします。
㌰太郎 = ""
uh = ㌰太郎.{concat}
🍎 = "apple"
✒️ = "pen"
[🍎, ✒️].each(&uh)
puts ㌰太郎.to_s
# applepen
parse.yを読む
Rubyのシンタックスはparse.yで定義されています。
parse.yを読んで以下の表現がわかれば.{}
演算子を定義できそうです。
- レシーバー
.
- メソッド名
-
{
と}
parse.yはだいたい1万行ちょっとあるうち半分ぐらいまでsyntaxが定義されててその下にトークン切り出す処理があります。
色々定義されてるとこにはRipper向けのコードが挟まっているので雑にsyntax足すなら1万行のうちの前半分、それぞれの定義の上の方だけ読めばよさそうです
なまえ : トークン1 トークン2
{
/*%%%*/
$$ = じっこうされるやつ
/*%
$$ = Ripperのためのもろもろ
%*/
}
;
メソッド名の表し方
とりあえずメソッド名の定義のあたりを探します。メソッド名を定義するときはdefと書くのでdefでgrepします。
% git grep -n '"def"' parse.y
parse.y:3129: token_info_push("def");
parse.y:11176: {keyword_def, "def"},
keyword_def
がでてきました。それっぽいのでプロジェクト内で検索するとk_def
が出てきたので更にk_def
で検索します。
k_def : keyword_def
{
token_info_push("def");
/*%%%*/
$<num>$ = ruby_sourceline;
/*%
%*/
}
;
k_def
の次に来るのはfname
のようです。fname
をしらべれば、メソッド名を表す何かがわかりそうです。
primary : literal
/* 中略 */
| k_def fname
{
local_push(0);
$<id>$ = current_arg;
current_arg = 0;
}
{
$<num>$ = in_def;
in_def = 1;
}
f_arglist
bodystmt
k_end
{
/*%%%*/
NODE *body = remove_begin($6);
reduce_nodes(&body);
$$ = NEW_DEFN($2, $5, body, METHOD_VISI_PRIVATE);
nd_set_line($$, $<num>1);
/*%
$$ = dispatch3(def, $2, $5, $6);
%*/
local_pop();
in_def = $<num>4 & 1;
current_arg = $<id>3;
}
おっ、fname
がメソッド名を表してるっぽい。Rubyの場合、再定義できる演算子があるのでメソッド名っぽいtIDENTIFIER
の他にop
や予約語なども引数に取れるようですね。
methodメソッドでは引数に:+
など演算子のメソッド名や大文字のメソッド名:Integer
も取れるのでtIDENTIFIER
やop
やtCONSTANT
はつかえそうですが予約語のreswords
は必要なさそうですね。
fname : tIDENTIFIER
| tCONSTANT
| tFID
| op
{
SET_LEX_STATE(EXPR_ENDFN);
$$ = $1;
}
| reswords
{
SET_LEX_STATE(EXPR_ENDFN);
/*%%%*/
$$ = $<id>1;
/*%
$$ = $1;
%*/
}
;
/* 中略 */
reswords : keyword__LINE__ | keyword__FILE__ | keyword__ENCODING__
| keyword_BEGIN | keyword_END
| keyword_alias | keyword_and | keyword_begin
| keyword_break | keyword_case | keyword_class | keyword_def
| keyword_defined | keyword_do | keyword_else | keyword_elsif
| keyword_end | keyword_ensure | keyword_false
| keyword_for | keyword_in | keyword_module | keyword_next
| keyword_nil | keyword_not | keyword_or | keyword_redo
| keyword_rescue | keyword_retry | keyword_return | keyword_self
| keyword_super | keyword_then | keyword_true | keyword_undef
| keyword_when | keyword_yield | keyword_if | keyword_unless
| keyword_while | keyword_until
;
tIDENTIFIER
で探すと今回の目的に合ってそうな定義がでてきました。
operation2 : tIDENTIFIER
| tCONSTANT
| tFID
| op
;
捕捉: 書いてる時点ではreswords
いらないと思ってたけどこういうプログラムも書けるからいるっぽい
def nil
puts 'hi'
end
self.send(:nil)
メソッドよびだし
メソッド呼び出し時のレシーバーや.
やメソッド名の表し方を確認するため定義を探します。メソッド呼び出しは.
が間にあるので.
で探してみます。
# git grep -n '"."' parse.y`ではうまくgrepできなかった
% git grep -n "'\.'" parse.y
parse.y:4976:dot_or_colon : '.'
parse.y:4988:call_op : '.'
parse.y:4991: $$ = '.';
parse.y:4993: $$ = ripper_id2sym('.');
parse.y:6064: case '$': case '*': case '+': case '.':
parse.y:6275: BIT(';', idx) | BIT(',', idx) | BIT('.', idx) | BIT('=', idx) | \
parse.y:6606: if (c == '.') {
parse.y:7363: else if (c == '.' || c == 'e' || c == 'E') {
parse.y:7381: case '.':
parse.y:7395: tokadd('.');
parse.y:7722: case '.': /* $.: last read line number */
parse.y:8051: case '.': {
parse.y:8053: if (peek('.') == (c == '&')) {
parse.y:8286: else if (c == '.') {
parse.y:8386: case '.':
parse.y:8388: if ((c = nextc()) == '.') {
parse.y:8389: if ((c = nextc()) == '.') {
parse.y:8400: return '.';
call_op
が怪しいのでそれで検索すると、それっぽいのがでました。レシーバーはprimary_value
と書き、メソッド呼び出しの.
はcall_op
と呼ばれているようです。
method_call : fcall paren_args
/* 中略 */
| primary_value call_op operation2
{
/*%%%*/
$<num>$ = ruby_sourceline;
/*% %*/
}
opt_paren_args
{
/*%%%*/
$$ = NEW_QCALL($2, $1, $3, $5);
nd_set_line($$, $<num>4);
/*%
$$ = dispatch3(call, $1, $2, $3);
$$ = method_optarg($$, $5);
%*/
}
{
と }
の表現
grepしたらすぐみつかった。
% git grep -n "'{'" parse.y
parse.y:1068: '{' top_compstmt '}'
parse.y:1168: '{' top_compstmt '}'
parse.y:1280: | keyword_END '{' compstmt '}'
parse.y:3720:brace_block : '{'
parse.y:5771: if (peek('{')) { /* handle \u{...} form */
parse.y:6144: if (c2 == '$' || c2 == '@' || c2 == '{') {
parse.y:6312: case '{':
parse.y:7582: else if (term == '{') term = '}';
parse.y:8541: case '{':
parse.y:8554: c = '{'; /* block (primary) */
普通に'{'
と'}'
を書けばよさそうですね!
top_stmt : stmt
| keyword_BEGIN
{
/*%%%*/
/* local_push(0); */
/*%
%*/
}
'{' top_compstmt '}'
{
/*%%%*/
ruby_eval_tree_begin = block_append(ruby_eval_tree_begin,
$4);
/* NEW_PREEXE($4)); */
/* local_pop(); */
$$ = NEW_BEGIN(0);
/*%
$$ = dispatch1(BEGIN, $4);
%*/
}
;
/* 中略 */
stmt_or_begin : stmt
{
$$ = $1;
}
| keyword_BEGIN
{
yyerror("BEGIN is permitted only at toplevel");
/*%%%*/
/* local_push(0); */
/*%
%*/
}
'{' top_compstmt '}'
{
/*%%%*/
ruby_eval_tree_begin = block_append(ruby_eval_tree_begin,
$4);
/* NEW_PREEXE($4)); */
/* local_pop(); */
$$ = NEW_BEGIN(0);
/*%
$$ = dispatch1(BEGIN, $4);
%*/
}
/* 中略 */
brace_block : '{'
{
/*%%%*/
$<num>$ = ruby_sourceline;
/*% %*/
}
brace_body '}'
{
$$ = $3;
/*%%%*/
nd_set_line($$, $<num>2);
/*% %*/
}
| keyword_do
{
/*%%%*/
$<num>$ = ruby_sourceline;
/*% %*/
}
do_body keyword_end
{
$$ = $3;
/*%%%*/
nd_set_line($$, $<num>2);
/*% %*/
}
;
parse.yを書く
parse.yをみるとprimary_value
とcall_op
と'{'
とoperation2
と'}'
を組み合わせればそれっぽいsyntaxが定義できそうなので書いてみましょう。
書く場所
どこに書くか悩むんですがメソッド呼び出しやλぽい定義あったんでそのあたりに足します
primary : literal
/* 中略 */
| method_call
| method_call brace_block
{
/*%%%*/
block_dup_check($1->nd_args, $2);
$2->nd_iter = $1;
$$ = $2;
/*%
$$ = method_add_block($1, $2);
%*/
}
/* このへん? */
| tLAMBDA lambda
{
$$ = $2;
}
他の部分の真似をして付け足します。
| primary_value call_op '{' operation2 '}'
{
ここに実装を書く
}
メソッド呼び出しの実装を読む
object.{method}
と書いたとき、Objectに対してmethodメソッドを呼び出したいので参考のためにmethod_callの実装を読みます。
method_call : fcall paren_args
{
/* 略 */
}
| primary_value call_op operation2
{
/*%%%*/
$<num>$ = ruby_sourceline;
/*% %*/
}
opt_paren_args
{
/*%%%*/
/*
$$ = NEW_QCALL($2, $1, $3, $5);
nd_set_line($$, $<num>4);
/*%
/* ここからしたはRipper向け定義のよう */
$$ = dispatch3(call, $1, $2, $3);
$$ = method_optarg($$, $5);
%*/
}
定義してる部分の最初のトークンから順番よく$数字
と入っていくようです。
method_callの定義の場合はprimary_value
が$1
に、call_op
が$2
に、operation2
が$3
に、ruby_source_line
が$4
に、opt_paren_args
が$5
でしょうか。
実装部分ではNEW_QCALL(call_op, primary_value, operation2, opt_paren_args)
の結果を$$
に代入していますね。
NEW_QCALL
の定義を読むと、NEW_QCALL
ではぼっち演算子(&.
)でメソッドが呼ばれたときに対応するための処理が入り、最終的にNODE_CALL
が呼ばれているようです。
#define CALL_Q_P(q) ((q) == tANDDOT)
#define NODE_CALL_Q(q) (CALL_Q_P(q) ? NODE_QCALL : NODE_CALL)
#define NEW_QCALL(q,r,m,a) NEW_NODE(NODE_CALL_Q(q),r,m,a)
NODE_CALL
の使い方を調べてみましょう。以下のような関数がありました。
static NODE *
call_bin_op_gen(struct parser_params *parser, NODE *recv, ID id, NODE *arg1)
{
value_expr(recv);
value_expr(arg1);
return NEW_CALL(recv, id, NEW_LIST(arg1));
}
methodメソッドを呼ぶ場合、NODE_CALL
に第一引数にレシーバー、第二引数のidにmethodメソッドのID、第三引数にNEW_LIST(methodメソッドに渡す引数のNODE)
を渡すとよさそうです。
それでは実装してみましょう。
書く
まずはNEW_QCALLを呼び出しましょう。第1引数にはcall_opを渡し、第2引数にreceiverを渡します。
| primary_value call_op '{' operation2 '}'
{
$$ = NEW_QCALL($2, $1, methodメソッドのID, NEW_LIST(methodメソッドの引数のNODE);
}
methodメソッドをのIDを取る方法を調べます。「IDを返す」で検索すると以下の文書が引っかかりました。rb_intern
を使うとよさそうです
ID rb_intern(const char *name) ::
文字列に対応するIDを返す.
| primary_value call_op '{' operation2 '}'
{
$$ = NEW_QCALL($2, $1, rb_intern("method"), NEW_LIST(methodメソッドの引数のNODE);
}
次はoperation2からmethodメソッドの引数のNODEを作ります。methodメソッドの引数はシンボルなのでシンボルの定義を読むとNEW_LIT(ID2SYM(〜))
でシンボルが出来るようです。
literal : numeric
| symbol
{
/*%%%*/
$$ = NEW_LIT(ID2SYM($1));
/*%
$$ = dispatch1(symbol_literal, $1);
%*/
}
| dsym
;
念のためNEW_LIT
がNODEを返すか定義を確認
#define NEW_LIT(l) NEW_NODE(NODE_LIT,l,0,0)
あとは.{}
演算子の定義のoperation2、$4
を引数わたしてシンボル化するとよさそうですね!
| primary_value call_op '{' operation2 '}'
{
$$ = NEW_QCALL($2, $1, rb_intern("method"), NEW_LIST(NEW_LIT(ID2SYM($4))));
}
さっそくmakeしてみます。parse.yからparse.cが生成されコンパイルされます。
Ripperの定義を省いたのでripperのビルドはコケましたが、parse.yはうまくコンパイル出来たようです!
% make
(略)
generating parse.c
compiling parse.c
linking miniruby
(略)
compiling ripper.c
ripper.y: In function 'ripper_yyparse':
ripper.y:2798:17: warning: assignment makes integer from pointer without a cast [-Wint-conversion]
$$ = NEW_QCALL($2, $1, rb_intern("method"), NEW_LIST(NEW_LIT(ID2SYM($4))));
^
linking shared-object ripper.bundle
checking ../.././parse.y and ../.././ext/ripper/eventids2.c
*** Following extensions failed to configure:
../.././ext/openssl/extconf.rb:0: Failed to configure openssl. It will not be installed.
*** Fix the problems, then remove these directories and try again if you want.
(略)
早速確認用のプログラムを動かしてみましょう。
% cat method_method_syntax.rb
㌰太郎 = ""
uh = ㌰太郎.{concat}
🍎 = "apple"
✒️ = "pen"
[🍎, ✒️].each(&uh)
puts ㌰太郎.to_s
% ./ruby --disable=gems method_method_syntax.rb
method_method_syntax.rb:2: syntax error, unexpected {, expecting '('
uh = ㌰太郎.{concat}
^
あれ、エラーが出ました。unexpected {
と言われて閉まっています。{
がうまく認識されてなさそうです。
他の{
を使うsyntaxを確認するとHashの記法の定義ではtLBRACE
を使っているようです。
| tLBRACE assoc_list '}'
{
/*%%%*/
$$ = new_hash($2);
/*%
$$ = dispatch1(hash, escape_Qundef($2));
%*/
}
Hashと同じようにtLBRACEで置き換えてみます。
| primary_value call_op tLBRACE operation2 '}'
{
$$ = NEW_QCALL($2, $1, rb_intern("method"), NEW_LIST(NEW_LIT(ID2SYM($4))));
}
コンパイルして再実行
% make
% % ./ruby --disable=gems method_method_syntax.rb
applepen
動きました
まとめ
parse.yを読み書きして.{}
演算子を追加してみました
parse.yいじると普段変更できないsyntaxを変更出来るので楽しいですね
Okinawa.rbについて
Okinawa.rbのAdvent Calendar 2016 は記事を読む人・書く人を募集しています。
Okinawa.rbは参加者も募集しています
開催情報はDoorkeeperに載っています。毎月どこかの週の水曜日の夜に開催しています
https://okinawarb.doorkeeper.jp
流れ的には雑談、初対面の人がいるときは自己紹介、もくもく、解散です。(僕はもくもくとRubyのソースを読んだりしています)
次回は2016-12-14(水)19:00 - 22:00です。