スクリプト解析は文字列を解析する関数とファイルを解析する関数がありますが、それらの違いは所詮、入力の違いなのでmrb_parse_string()をスクリプト解析のエントリポイントとして読み進めていきたいと思います。
mrb_parse_string()は渡された文字列をstrlenしてmrb_parse_nstring()に渡しているだけなのでmrb_parse_nstring()を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
| - | | | | | | | | | ! |
|
スクリプト解析で鍵となるのはmrb_parser_state構造体のようです。mrb_parser_stateはinclude/compile.hに書かれています。なお、上記ではparser_stateになっていますが、parser_stateはmrb_parser_stateをtypedefしたものです。
次にmrb_parser_parse()を見てみます。ちょっと長めですが全部貼り付け。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| - | | - | | | | ! | | | | | | | | - - | ! - | ! ! - - | ! - | | ! ! ! |
|
関数を眺めると、
yyparse()を実行するとmrb_parser_state.treeに解析結果が格納される
ということがわかります。node(mrb_ast_nodeがtypedefされたもの)は上記のmrb_parser_parse()を見てもわかるとおり、LISPのリスト構造と同じになっています。*1
1
2
3
| - | ! |
|
CRubyのNODE構造体に比べると驚くほどシンプルです。シンプルな分は運用で回避。parse.yの前半部分にはリスト操作、およびそれを使って解析ツリーを作るための関数群が定義されています。例えばこんな感じ、
具体的にスクリプトをNODEに変換してみましょう。対象とするスクリプトはRuby1.9のスクリプト解析で変換してみたものと同じスクリプトを使います*2。
class MonteCarlo def pi(n) count = 0 (1..n).each do x = rand y = rand if x * x + y * y <= 1 count += 1 end end (count.to_f / n) * 4 end end n = 10000 * 10000 pi = MonteCarlo.new.pi(n) puts "pi = #{pi}"
bin/mrubyは--verboseオプションを付けるとNODEツリーがダンプされます*3。上記のスクリプトを食わせると以下のNODEツリーが出力されました。
$ ./mruby.exe -c --verbose ../../montecarlo.rb NODE_SCOPE: local variables: n pi NODE_BEGIN: NODE_CLASS: :MonteCarlo body: NODE_BEGIN: NODE_DEF: pi local variables: n count mandatory args: NODE_ARG n NODE_BEGIN: NODE_ASGN: lhs: NODE_LVAR count rhs: NODE_INT 0 base 10 NODE_CALL: NODE_BEGIN: NODE_DOT2: NODE_INT 1 base 10 NODE_LVAR n method='each' (170) args: block: NODE_BLOCK: body: NODE_BEGIN: NODE_ASGN: lhs: NODE_LVAR x rhs: NODE_CALL: NODE_SELF method='rand' (330) NODE_ASGN: lhs: NODE_LVAR y rhs: NODE_CALL: NODE_SELF method='rand' (330) NODE_IF: cond: NODE_CALL: NODE_CALL: NODE_CALL: NODE_LVAR x method='*' (80) args: NODE_LVAR x method='+' (76) args: NODE_CALL: NODE_LVAR y method='*' (80) args: NODE_LVAR y method='<=' (300) args: NODE_INT 1 base 10 then: NODE_BEGIN: NODE_OP_ASGN: lhs: NODE_LVAR count op='+' (76) NODE_INT 1 base 10 NODE_CALL: NODE_BEGIN: NODE_CALL: NODE_CALL: NODE_LVAR count method='to_f' (109) method='/' (148) args: NODE_LVAR n method='*' (80) args: NODE_INT 4 base 10 NODE_ASGN: lhs: NODE_LVAR n rhs: NODE_CALL: NODE_INT 10000 base 10 method='*' (80) args: NODE_INT 10000 base 10 NODE_ASGN: lhs: NODE_LVAR pi rhs: NODE_CALL: NODE_CALL: NODE_CONST MonteCarlo method='new' (6) method='pi' (326) args: NODE_LVAR n NODE_CALL: NODE_SELF method='puts' (286) args: NODE_DSTR NODE_STR "pi = " len 5 NODE_BEGIN: NODE_LVAR pi NODE_STR "" len 0
それではどういうルールを通ることでこのようなNODEツリーが構築されるのかを追っていくことにします。
まずクラス定義があります。これは、program → top_compstmt → top_stmts → top_stmt → stmt → expr → arg → primary → keyword_class、とルールが進んでいきます。忘れてましたがそういえばRubyではクラス定義も式でしたね。
次のメソッド定義は、bodystmt → compstmt → stmts → stmt → expr → arg → primary → keyword_defと進みます。
続いて、引数記述の解析に移ります。Rubyは、通常引数、オプション引数、残余引数、ブロック引数と引数の種類がたくさんあるので引数解析のルールも様々なパターンが書かれています。
今回は通常引数のみの一番単純なケースなので、f_arglist → f_args → f_arg → f_arg_item → f_norm_arg、とルールが進んでいきます。
代入は飛ばしてブロック付きメソッド実行に移ります。
メソッド呼び出し全体にマッチするルールは「method_call brace_block」です。brace_blockとなっていますが、do〜endを使った場合もマッチします。
次に「(1..n).each」の部分はmethod_callの「primary_value '.' operation2 opt_paren_args」にマッチします。
最後に(1..n)の部分は、primary_value → primary → tLPAREN compstmt ')' → (中略) → arg → arg tDOT2 arg、となります。
「method_call brace_block」の際に呼ばれるcall_with_block()を見てみましょう。
method_call brace_block { call_with_block(p, $1, $2); $$ = $1; }
慣れないとわかりづらいですが、carはリストの先頭要素、cdrはリストの先頭を除いたリストです。cdrのcdrのcdrは元のリストの前3要素を除いたリストになります。例えば元リストが(a b c d)とすると結果は以下のようになります。
で、aとはNODE_CALLなので、
primary_value '.' operation2 opt_paren_args { $$ = new_call(p, $1, $3, $4); }
call_with_block()中のnは引数情報を指すことがわかります。
Ruby1.9のスクリプト解析のNODEと比較するとCRubyとmrubyで構築されるノードと微妙に違うことがわかります。
というわけでmrubyのスクリプト解析を見てきました。わかったこととしては、
といったところでしょうか。