前回、JRubyの初期化(org.jruby.Rubyインスタンスの初期化)まで読んだので、続いてRubyスクリプトを読み込んで実行する部分を読みたいと思います。
スクリプト解析、実行のエントリポイントはrunFromMainメソッドです。runFromMainメソッドから処理を追ってみると以下の流れとなっているようです。
それでは個々に見ていきましょう。
スクリプトの解析を行っているのはorg.jruby.parser.Parserクラスです。ただし、Parserクラスはただのファサードで実際の処理はRubyParserインターフェース実装が利用されるようです。これは、JRubyでは1.8、1.9、2.0と複数のRubyバージョンをサポートしているためバージョンごとにパーサが用意されている、というわけなようです。
それでは具体的にRuby19Parserを見てみましょう。Ruby19Parser.javaを見ると人が書いたと思えないようなコードが記述されていますがRuby19Parser.javaは実際、人が書いたわけではなくjayというJava版のyaccを使って生成されたもので元となるのは同じディレクトリにあるRuby19Parser.yです。こちらにはBNFとアクションが記述されています。
さて、構文解析を行う際に利用されるのはParserSupportクラスとRubyYaccLexerでそれぞれノードの構築補助と字句解析を担当しているようです。
例によって実際のスクリプトを使って構文解析の流れを追ってみます。対象とするスクリプトは毎度おなじみmontecarlo.rbです。
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}"
それではマッチする規則を見ていくことにしましょう。
クラス定義はprimary規則のうち、「kCLASS cpath superclass」がマッチします。この規則では以下3つのNodeをClassNodeとしてまとめています。
cpath規則ではcnameのみのものがマッチするため、ParserSupport.new_colon2メソッドが呼び出されます。その結果、Colon2ImplicitNodeが構築されます。
メソッド定義はprimary規則のうち、「kDEF fname」がマッチします。この規則では以下2つのNodeをDefnNodeとしてまとめています。
ちなみに、メソッド名もNodeになっていますがこれは定義位置を記録するためなようです。
引数の部分はf_args規則のうち、「f_arg opt_f_block_arg」がマッチします。なお、opt_f_block_argはnullになります。引数はParserSupport.new_argsメソッドが用いられます。new_argsメソッドを見てみると最適化なのでしょうか単純な引数の場合には特殊化されたNodeが作られるようです。今回は引数1つで代入は行わないのでArgsPreOneArgNodeが構築されます。
ところで代入が行われるって、それなんてオプション引数な気がするのですが起こりうるのでしょうか。
f_arg_item規則により個々の引数に対してArgumentNodeが作られ、f_argでそれらがArrayNodeにまとめられています。
なお、引数はローカル変数として現在のスコープに追加され、ArgumentNodeではスロット番号が設定されます。
次は「count = 0」の部分です。arg規則のうち、「lhs '=' arg」がマッチします。
lhsではParserSupport.assignableメソッドが呼び出されます。今回はローカル変数(tIDENTIFIER)なのでStaticScope.assignに処理が委譲されます。で、StaticScope.assignメソッド
現在のスコープがブロック内なのかブロック外なのかで作られるNodeが違うようです。今回はブロック外なのでLocalStaticScopeのassignメソッドに委譲されLocalAsgnNodeが構築されます。
「count = 0」のうち0の部分はarg → primary → literal → numeric → tINTEGERと規則が進みます。tINTEGERはRubyYaccLexerクラスでNodeが作られることになります。今回は値が小さいのでFixNumNodeが作られます。
次は「(1..n).each do」です。でもその前に複数の文をどう接続しているかについて。ParserSupport.appendToBlockメソッドが呼ばれることによりBlockNodeが構築されます。BlockNodeでは個々の文を子要素として保持します。
で、「(1..n).each do」です。まず、(1..n)の部分について説明します。()はprimary規則の「tLPAREN compstmt tRPAREN」がマッチします。「1..n」はarg規則のうち「arg tDOT2 arg」がマッチしDotNodeが構築されます。
1つ目のargは先ほどと同様FixNumNodeになります。2つ目のargはvar_ref規則がマッチしParserSupport.gettableメソッドが呼び出されます。StaticScope.declareメソッド → LocalStaticScopeメソッドと処理が委譲されLocalVarNodeが構築されます。
「each do」の部分はprimary規則のうち「method_call brace_block」がマッチします。
method_call規則では「primary_value tDOT operation2 opt_paren_args」がマッチします。primary_valueは上記のDotNode、opt_paren_argsは引数がないのでnullです。Nodeの構築にはParserSupport.new_callメソッドが呼ばれます。引数の数や種類によって構築されるNodeに違いが出るようです。今回はCallNoArgNodeが構築されるようです。
brace_blockの中身はこれから見ますがブロック本体はIterNodeとしてまとめられるようです。
ブロックの始めにあるのは「x = rand」です。先ほどの代入と同様arg規則の「lhs '=' arg」がマッチしますが動作が異なります。BlockStaticScopeのassignメソッドが呼ばれ、
1
2
3
4
5
6
7
8
9
10
| - | - | | | | | ! ! |
|
xはまだ定義されていないので一番下のenclosingScope.assignが呼ばれます。で、enclosingScopeはLocalStaticScopeなので
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| - | - | | - | - | - | | | ! | - | ! ! ! |
|
ブロック外にもxはないのでBlockStaticScopeに戻ってxが追加されます。
次にrandの部分ですがここは注意が必要です。人間は知識があるのでrandは引数なし、括弧なしの関数呼び出しであるということがわかりますがパーサにはわかりません。というわけでパーサ的にはrandはvar_refのuser_variableの場合にマッチします。そこでParserSupport.gettableメソッドが呼び出され
1
2
3
4
5
6
7
8
9
| - - | | | | | ! ! |
|
スコープ内にrandはないのでenclosingScopeに委譲。
1
2
3
4
5
6
7
8
9
| - - | | | | | ! ! |
|
enclosingScopeでも定義されてない → 関数呼び出ししてみる、ということでVCallNodeが構築されます。VCallNodeのクラス定義を見ると単純に関数名を保持するだけではなくキャッシュ処理が行われているようですがスクリプト解析の時点では関係ないことなので無視します。
次は「if x * x + y * y <= 1」です。primary規則の「kIF expr_value then compstmt if_tail kEND」がマッチします。以下3つのNodeをまとめてIfNodeが構築されます。
条件部の「x * x + y * y <= 1」はarg規則にある「arg tSTAR2 arg」や「arg tPLUS arg」がマッチしCallOneArgNodeが構築されます。また、xやyについてはvar_refがマッチし、ブロックで定義された変数なのでDVarNodeが構築されます。
真の場合の実行文「count += 1」はarg規則の「var_lhs tOP_ASGN arg」がマッチします。var_lhsはLocalAsgnNodeになります。ちなみにRuby1.9だとブロック外のローカル変数を取得する場合でもDASGNが使われてますね。
途中飛ばして「puts "pi = #{pi}"」です。関数呼び出しに括弧がないのでcommand規則の「operation command_args」がマッチします。で、ParserSupport.new_fcallメソッドが呼ばれ引数の条件からFCallOneArgNodeが構築されます。
次に引数の"pi = #{pi}"の部分です。RubyYaccLexer.yylexメソッドでは「"」にぶつかるとlex_strtermフィールド(StrTerm型)にStringTermオブジェクトを設定します。次にyylexが呼ばれると設定したlex_strtermのparseStringメソッドを呼び出すことで文字列の解析が行われます。
StringTerm.parseStringメソッドでは「#」を見つけると式展開の処理が必要なことを判断します。まず、「pi = 」の部分がStrNodeとして切り出されることになります。次にparseStringメソッドが呼ばれるとメソッドはtSTRING_DBEGを返します。これにより、string_content規則の「tSTRING_DBEG compstmt tRCURLY」がマッチしEvStrNodeが構築されます。最後にstring_contents規則のアクションであるParserSupport.literal_concatメソッドが実行されDtrNodeを構築、StrNode(固定文字列部分)とEvStrNode(式展開部分)がまとめられます。
以上でスクリプトの解析(Nodeへの変換)は終了です。変換結果はこちらになります。montecarlo.node.txt
今回はJRubyのうち、スクリプト解析部分を読んでいきました。当然と言えば当然ですがスクリプト解析はRuby1.9とほぼ同じでした。一方でオブジェクト指向が活かされているなという部分もありました。
スクリプト解析はさらっと流してコンパイルまで書こうと思ったのですがCRubyを知らない人にはそれも不親切なので毎度のことながら手動yaccを行いました。次は当初の目的であるRubyスクリプトのJavaバイトコードへの変換を見ていきたいと思います。