LoginSignup
5
2

More than 5 years have passed since last update.

Rubyに雑に演算子を足す

Last updated at Posted at 2016-12-10

:hibiscus: Okinawa.rbのAdvent Calendar 2016 :pig2: 10日目です。

RubyでMethodオブジェクトを取り出すsyntaxが欲しいときってありますよね。ruby-coreでo.:methodo[.method]o->methodなどが提案されていますがしっくりこないみたいです。
https://bugs.ruby-lang.org/issues/12125#change-57209

Rubyのsyntaxをいじる練習に提案されているsyntaxとは別のsyntaxを雑に実装してみようと思います。

:bulb: どういうsyntaxを実装するか

ここにオブジェクトがあります。

o = Object.new

オブジェクトのmethodメソッドの取り出し方を考えます。

とりだすメソッドの名前を指定しないと取り出せないので、とりあえずmethodと書く必要はあるでしょう。

o /*何かしらのsyntax*/ method

るりまのRubyで使われる記号の意味リテラルのページを見ると記号はだいたい使われていそうです。

RubyではProcをつくるときに-> { }、ブロックを作るときにfoo { }のように{}を使うことが多いですね。methodメソッドもProcやブロックと同じようにcallできるので同じように{}を使って書きたいです。
なので、今回はreceiver.{method}でMethodオブジェクトを取り出せるようにしてみましょう。

以降.{}演算子と呼びます。

:wrench: 準備

% 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を読んでやっていき :muscle:
https://github.com/ruby/ruby#how-to-compile-and-install

:apple::pen_fountain: テスト

雑に足すだけなので以下でapplepenと表示されればいいことにします。

㌰太郎 = ""
uh = ㌰太郎.{concat}

🍎 = "apple"
✒️ = "pen"

[🍎, ✒️].each(&uh)

puts ㌰太郎.to_s
# applepen

:eyes: parse.yを読む

Rubyのシンタックスはparse.yで定義されています。

parse.yを読んで以下の表現がわかれば.{}演算子を定義できそうです。

  • レシーバー
  • .
  • メソッド名
  • {}

parse.yはだいたい1万行ちょっとあるうち半分ぐらいまでsyntaxが定義されててその下にトークン切り出す処理があります。
色々定義されてるとこにはRipper向けのコードが挟まっているので雑にsyntax足すなら1万行のうちの前半分、それぞれの定義の上の方だけ読めばよさそうです :ok_hand:

parse.y
なまえ : トークン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で検索します。

parse.y
k_def       : keyword_def
            {
            token_info_push("def");
            /*%%%*/
            $<num>$ = ruby_sourceline;
            /*%
            %*/
            }
        ;

k_defの次に来るのはfnameのようです。fnameをしらべれば、メソッド名を表す何かがわかりそうです。

parse.y
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も取れるのでtIDENTIFIERoptCONSTANTはつかえそうですが予約語のreswordsは必要なさそうですね。

parse.y
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で探すと今回の目的に合ってそうな定義がでてきました。

parse.y
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と呼ばれているようです。

parse.y
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) */

普通に'{''}'を書けばよさそうですね!

parse.y
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);
            /*% %*/
            }
        ;

:pencil: parse.yを書く

parse.yをみるとprimary_valuecall_op'{'operation2'}'を組み合わせればそれっぽいsyntaxが定義できそうなので書いてみましょう。

書く場所

どこに書くか悩むんですがメソッド呼び出しやλぽい定義あったんでそのあたりに足します

parse.y
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;
            }

他の部分の真似をして付け足します。

parse.y
        | primary_value call_op '{' operation2 '}'
            {
                ここに実装を書く
            }

メソッド呼び出しの実装を読む

object.{method}と書いたとき、Objectに対してmethodメソッドを呼び出したいので参考のためにmethod_callの実装を読みます。

parse.y
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が呼ばれているようです。

parse.y
#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の使い方を調べてみましょう。以下のような関数がありました。

parse.y
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を渡します。

parse.y
        | primary_value call_op '{' operation2 '}'
            {
                $$ = NEW_QCALL($2, $1, methodメソッドのID, NEW_LIST(methodメソッドの引数のNODE);
            }

methodメソッドをのIDを取る方法を調べます。「IDを返す」で検索すると以下の文書が引っかかりました。rb_internを使うとよさそうです :smiley:

doc/extension.ja.rdoc
ID rb_intern(const char *name) ::

  文字列に対応するIDを返す.
parse.y
        | primary_value call_op '{' operation2 '}'
            {
                $$ = NEW_QCALL($2, $1, rb_intern("method"), NEW_LIST(methodメソッドの引数のNODE);
            }

次はoperation2からmethodメソッドの引数のNODEを作ります。methodメソッドの引数はシンボルなのでシンボルの定義を読むとNEW_LIT(ID2SYM(〜))でシンボルが出来るようです。

parse.y
literal     : numeric
        | symbol
            {
            /*%%%*/
            $$ = NEW_LIT(ID2SYM($1));
            /*%
            $$ = dispatch1(symbol_literal, $1);
            %*/
            }
        | dsym
        ;

念のためNEW_LITがNODEを返すか定義を確認

node.h
#define NEW_LIT(l) NEW_NODE(NODE_LIT,l,0,0)

あとは.{}演算子の定義のoperation2、$4を引数わたしてシンボル化するとよさそうですね!

parse.y
        | 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を使っているようです。

parse.y
        | tLBRACE assoc_list '}'
            {
            /*%%%*/
            $$ = new_hash($2);
            /*%
            $$ = dispatch1(hash, escape_Qundef($2));
            %*/
            }

Hashと同じようにtLBRACEで置き換えてみます。

parse.y
        | 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

動きました :green_apple: :pen_fountain:

:sparkles::gem::sparkles: まとめ

parse.yを読み書きして.{}演算子を追加してみました:sparkles:
parse.yいじると普段変更できないsyntaxを変更出来るので楽しいですね :leopard:

:hibiscus: Okinawa.rbについて :pig2:

Okinawa.rbのAdvent Calendar 2016 は記事を読む人・書く人を募集しています。

Okinawa.rbは参加者も募集しています :airplane::pineapple:
開催情報はDoorkeeperに載っています。毎月どこかの週の水曜日の夜に開催しています :watermelon: :calendar_spiral:
https://okinawarb.doorkeeper.jp

流れ的には雑談、初対面の人がいるときは自己紹介、もくもく、解散です。(僕はもくもくとRubyのソースを読んだりしています)

次回は2016-12-14(水)19:00 - 22:00です。

参考

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2