#contents *はじめに [#c9f90f26] [[前回>JRuby/初期化を読む]]、JRubyの初期化(org.jruby.Rubyインスタンスの初期化)まで読んだので、続いてRubyスクリプトを読み込んで実行する部分を読みたいと思います。 *org.jruby.Ruby [#y7e981e6] **runFromMainメソッド [#ve598d94] スクリプト解析、実行のエントリポイントはrunFromMainメソッドです。runFromMainメソッドから処理を追ってみると以下の流れとなっているようです。 +parseFromMainメソッド:スクリプトの解析 +runNormallyメソッド:解析したスクリプトの実行 ++tryCompileメソッド:スクリプトをバイトコードへのコンパイル ++runScriptメソッド:コンパイルしたバイトコードの実行 それでは個々に見ていきましょう。 **parseFromMainメソッド [#yd68ef33] スクリプトの解析を行っているのは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でそれぞれノードの構築補助と字句解析を担当しているようです。 *解析してみる [#cb96d332] 例によって実際のスクリプトを使って構文解析の流れを追ってみます。対象とするスクリプトは毎度おなじみ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) [#hdf254ae] クラス定義はprimary規則のうち、「kCLASS cpath superclass」がマッチします。この規則では以下3つのNodeをClassNodeとしてまとめています。 :cpath|クラスのパスを表すNode :superclass|スーパークラスを表すNode :bodystmt|クラスを定義するNode ***cpath [#of612d85] cpath規則ではcnameのみのものがマッチするため、ParserSupport.new_colon2メソッドが呼び出されます。その結果、Colon2ImplicitNodeが構築されます。 **primary(kDEF fname) [#pc891b4a] メソッド定義はprimary規則のうち、「kDEF fname」がマッチします。この規則では以下2つのNodeをDefnNodeとしてまとめています。 :f_arglist|引数を表すNode :bodystmt|メソッドを定義するNode ちなみに、メソッド名もNodeになっていますがこれは定義位置を記録するためなようです。 **f_args(f_arg opt_f_block_arg) [#hf3432cc] ***f_args(f_arg opt_f_block_arg) [#hf3432cc] 引数の部分はf_args規則のうち、「f_arg opt_f_block_arg」がマッチします。なお、opt_f_block_argはnullになります。引数はParserSupport.new_argsメソッドが用いられます。new_argsメソッドを見てみると最適化なのでしょうか単純な引数の場合には特殊化されたNodeが作られるようです。今回は引数1つで代入は行わないのでArgsPreOneArgNodeが構築されます。 ところで代入が行われるって、それなんてオプション引数な気がするのですが起こりうるのでしょうか。 **f_argおよびf_arg_item [#t0ed2f6e] ***f_argおよびf_arg_item [#t0ed2f6e] f_arg_item規則により個々の引数に対してArgumentNodeが作られ、f_argでそれらがArrayNodeにまとめられています。 なお、引数はローカル変数として現在のスコープに追加され、ArgumentNodeではスロット番号が設定されます。 **arg(lhs '=' arg) ブロック外の場合 [#p39ba9c8] 次は「count = 0」の部分です。arg規則のうち、「lhs '=' arg」がマッチします。 lhsではParserSupport.assignableメソッドが呼び出されます。今回はローカル変数(tIDENTIFIER)なのでStaticScope.assignに処理が委譲されます。で、StaticScope.assignメソッド #code(Java){{ /** * Make a DASgn or LocalAsgn node based on scope logic * * @param position * @param name * @param value * @return */ public AssignableNode assign(ISourcePosition position, String name, Node value) { return assign(position, name, value, this, 0); } }} 現在のスコープがブロック内なのかブロック外なのかで作られるNodeが違うようです。今回はブロック外なのでLocalStaticScopeのassignメソッドに委譲されLocalAsgnNodeが構築されます。 「count = 0」のうち0の部分はarg → primary → literal → numeric → tINTEGERと規則が進みます。tINTEGERはRubyYaccLexerクラスでNodeが作られることになります。今回は値が小さいのでFixNumNodeが作られます。 **stmts(stmts terms stmt) [#bdae2826] 次は「(1..n).each do」です。でもその前に複数の文をどう接続しているかについて。ParserSupport.appendToBlockメソッドが呼ばれることによりBlockNodeが構築されます。BlockNodeでは個々の文を子要素として保持します。 **arg(arg tDOT2 arg) [#zeceb63d] で、「(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が構築されます。 **primary(method_call brace_block) [#gccd57e2] 「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としてまとめられるようです。 **arg(lhs '=' arg) ブロック内の場合 [#qc13336f] ブロックの始めにあるのは「x = rand」です。先ほどの代入と同様arg規則の「lhs '=' arg」がマッチしますが動作が異なります。BlockStaticScopeのassignメソッドが呼ばれ、 #code(Java){{ public class BlockStaticScope extends StaticScope { protected AssignableNode assign(ISourcePosition position, String name, Node value, StaticScope topScope, int depth) { int slot = exists(name); if (slot >= 0) return new DAsgnNode(position, name, ((depth << 16) | slot), value); return enclosingScope.assign(position, name, value, topScope, depth + 1); } } }} xはまだ定義されていないので一番下のenclosingScope.assignが呼ばれます。で、enclosingScopeはLocalStaticScopeなので #code(Java){{ public class LocalStaticScope extends StaticScope { public AssignableNode assign(ISourcePosition position, String name, Node value, StaticScope topScope, int depth) { int slot = exists(name); // We can assign if we already have variable of that name here or we are the only // scope in the chain (which Local scopes always are). if (slot >= 0) { return new LocalAsgnNode(position, name, ((depth << 16) | slot), value); } else if (topScope == this) { slot = addVariable(name); return new LocalAsgnNode(position, name, slot , value); } // We know this is a block scope because a local scope cannot be within a local scope // If topScope was itself it would have created a LocalAsgnNode above. return ((BlockStaticScope) topScope).addAssign(position, name, value); } } }} ブロック外にもxはないのでBlockStaticScopeに戻ってxが追加されます。 次にrandの部分ですがここは注意が必要です。人間は知識があるのでrandは引数なし、括弧なしの関数呼び出しであるということがわかりますがパーサにはわかりません。というわけでパーサ的にはrandはvar_refのuser_variableの場合にマッチします。そこでParserSupport.gettableメソッドが呼び出され #code(Java){{ public class BlockStaticScope extends StaticScope { public Node declare(ISourcePosition position, String name, int depth) { int slot = exists(name); if (slot >= 0) return new DVarNode(position, ((depth << 16) | slot), name); return enclosingScope.declare(position, name, depth + 1); } } }} スコープ内にrandはないのでenclosingScopeに委譲。 #code(Java){{ public class LocalStaticScope extends StaticScope { public Node declare(ISourcePosition position, String name, int depth) { int slot = exists(name); if (slot >= 0) return new LocalVarNode(position, ((depth << 16) | slot), name); return new VCallNode(position, name); } } }} enclosingScopeでも定義されてない → 関数呼び出ししてみる、ということでVCallNodeが構築されます。VCallNodeのクラス定義を見ると単純に関数名を保持するだけではなくキャッシュ処理が行われているようですがスクリプト解析の時点では関係ないことなので無視します。 **primary(kIF expr_value then compstmt if_tail kEND) [#b154361f] 次は「if x * x + y * y <= 1」です。primary規則の「kIF expr_value then compstmt if_tail kEND」がマッチします。以下3つのNodeをまとめてIfNodeが構築されます。 :expr_value|条件を表すNode :compstmt|条件が真の場合に実行されるNode :if_tail|条件が偽の場合に実行されるNode。elsifやelse 条件部の「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が使われてますね。 **command(operation command_args) [#w115b181] 途中飛ばして「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(式展開部分)がまとめられます。 **変換結果 [#b9c5940f] 以上でスクリプトの解析(Nodeへの変換)は終了です。変換結果はこちらになります。&ref(montecarlo.node.txt); *おわりに [#m06d2964] 今回はJRubyのうち、スクリプト解析部分を読んでいきました。当然と言えば当然ですがスクリプト解析はRuby1.9とほぼ同じでした。一方でオブジェクト指向が活かされているなという部分もありました。 -ローカルスコープ、ブロックスコープのオブジェクトを用意し変数検索処理 -引数の数、種類により構築するNodeを変える スクリプト解析はさらっと流してコンパイルまで書こうと思ったのですがCRubyを知らない人にはそれも不親切なので毎度のことながら手動yaccを行いました。次は当初の目的であるRubyスクリプトのJavaバイトコードへの変換を見ていきたいと思います。