今回はRubyのコードがどのような流れで実行されるのかをまとめます
rubyコマンドを実行してからコンソールに結果が出力されるまでに下記の手順を行います

それぞれの手順を細かく見ていきます
字句解析
字句解析とはソースコードを読み込んで、トークン列へと変換することです
Rubyコードは字句解析でトークン列に変換され、次の構文解析のプロセスを行います
ただし、全トークンを事前に生成するわけではなく、必要な分だけ順次トークン化していく(構文解析の中で字句解析が呼び出される形)ので字句解析は何回も行われます。
def hello(name)
puts "Hello, #{name}"
end
例えば上記の処理があるとします
- def → キーワード(tDEF)
- hello → 識別子(tIDENTIFIER)
- ( → 左括弧(tLPAREN)
- name → 識別子(tIDENTIFIER)
- ) → 右括弧(tRPAREN)
- 改行 → 終端記号(tNL)
- puts → 識別子(tIDENTIFIER)
- "Hello, #{name}" → 文字列リテラル(tSTRING)
- end → キーワード(tEND)
字句解析では、空白やコメントの除去、キーワードと識別子の区別、数値リテラル、文字列リテラルの認識、演算子や記号の認識、文字列補間の処理などを行います(文字列の読み取り、トークンの識別、トークンの分類)
元々は字句解析にはparse.yを使用していましたが、Ruby3.3以降はPrismを使用して字句解析を行っています
構文解析
構文解析とはRubyが分かるように、トークン列をグループ化する作業です
トークン列を文法のルールに従ってグループ化し、コードの構造を木構造(AST)で表現します
下記のようにDefNodeがdefの部分であり、ノードの開始となります。そこから木構造でノードを追加していきます。下記はPrismを使用した際のASTです
DefNode (メソッド定義ノード)
├─ name: "hello"
│ └─ type: Symbol
│ └─ value: :hello
│ └─ location: (1,4)-(1,9)
│
├─ parameters: ParametersNode (パラメータノード)
│ ├─ requireds: Array
│ │ └─ [0] RequiredParameterNode
│ │ ├─ name: "name"
│ │ ├─ type: Symbol
│ │ ├─ value: :name
│ │ └─ location: (1,10)-(1,14)
│ │
│ ├─ optionals: [] (空)
│ ├─ rest: nil
│ ├─ posts: [] (空)
│ ├─ keywords: [] (空)
│ ├─ keyword_rest: nil
│ └─ block: nil
│
├─ body: StatementsNode (文のリストノード)
│ └─ body: Array
│ └─ [0] CallNode (メソッド呼び出しノード)
│ ├─ receiver: nil (レシーバなし、トップレベル)
│ │
│ ├─ call_operator: nil
│ │
│ ├─ name: "puts"
│ │ └─ type: Symbol
│ │ └─ value: :puts
│ │
│ ├─ message_loc: (2,2)-(2,6)
│ │
│ ├─ opening_loc: nil (括弧なし)
│ │
│ ├─ arguments: ArgumentsNode (引数ノード)
│ │ └─ arguments: Array
│ │ └─ [0] InterpolatedStringNode (式展開文字列ノード)
│ │ ├─ opening_loc: (2,7)-(2,8) (")
│ │ │
│ │ ├─ parts: Array
│ │ │ ├─ [0] StringNode (通常文字列部分)
│ │ │ │ ├─ flags: FORCED_UTF8_ENCODING
│ │ │ │ ├─ content: "Hello, "
│ │ │ │ ├─ unescaped: "Hello, "
│ │ │ │ └─ location: (2,8)-(2,15)
│ │ │ │
│ │ │ └─ [1] EmbeddedStatementsNode (式展開部分)
│ │ │ ├─ opening_loc: (2,15)-(2,17) (#{)
│ │ │ │
│ │ │ ├─ statements: StatementsNode
│ │ │ │ └─ body: Array
│ │ │ │ └─ [0] LocalVariableReadNode
│ │ │ │ ├─ name: "name"
│ │ │ │ ├─ type: Symbol
│ │ │ │ ├─ value: :name
│ │ │ │ ├─ depth: 0 (スコープの深さ)
│ │ │ │ └─ location: (2,17)-(2,21)
│ │ │ │
│ │ │ ├─ closing_loc: (2,21)-(2,22) (})
│ │ │ └─ location: (2,15)-(2,22)
│ │ │
│ │ ├─ closing_loc: (2,22)-(2,23) (")
│ │ └─ location: (2,7)-(2,23)
│ │
│ ├─ closing_loc: nil
│ ├─ block: nil (ブロックなし)
│ ├─ flags: IGNORE_VISIBILITY
│ └─ location: (2,2)-(2,23)
│
├─ locals: ["name"] (ローカル変数のリスト)
│
├─ def_keyword_loc: (1,0)-(1,3) (def)
│
├─ operator_loc: nil
│
├─ lparen_loc: (1,9)-(1,10) (()
│
├─ rparen_loc: (1,14)-(1,15) ())
│
├─ equal_loc: nil
│
├─ end_keyword_loc: (3,0)-(3,3) (end)
│
└─ location: (1,0)-(3,3) (全体の位置)
ちなみに、構文解析器は再帰下降パーサーまたはLR(LALR)パーサーの手法を使っています
コンパイル
コンパイルとは、コードをプログラミング言語から別の言語(ターゲット言語)に変えることです
Rubyのインタプリタがコードを読み込み、木構造(AST)からYARVのバイトコード命令列に変換します
AST構造をYARV命令にコンパイルするために、Rubyは木構造の一番上から再帰的にツリーを反復しながら、それぞれのASTノードをYARY命令に変換していきます
ASTノードにはノードタイプが定義されているので、ノードタイプに一致するYARY命令に変換します。例えば、NODE_CALLはputself + opt_send_without_blockに変換されます
コンパイルすると下記のようになります
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(3,3)> (catch: FALSE) 0000 putspecialobject 1 ( 1)[Li] 0002 putobject :hello 0004 putiseq hello 0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache> 0009 leave == disasm: #<ISeq:hello@<compiled>:1 (1,0)-(3,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] name@0<Arg> 0000 putself ( 2)[LiCa] 0001 putobject "Hello, " 0003 getlocal_WC_0 name@0 0005 dup 0006 checktype T_STRING 0008 branchif 15 0010 dup 0011 opt_send_without_block <callinfo!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>, <callcache> 0014 tostring 0015 concatstrings 2 0017 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache> 0020 leave ( 3)[Re]
1つ目の命令列はメソッド定義部分で、2つ目の命令列はメソッド本体部分を表しています
コンパイラはまず、ASTのルートノードを見てDefNode(メソッド定義)を発見します。DefNodeを発見するとメソッド本体(body部分)を別の命令列(ISeq)としてコンパイルして、 トップレベルにはメソッドを定義する命令だけ生成します。
実行
補足程度ですが、コンパイルされたバイトコード命令列を実行するときにYARVがバイトコード実行するのですが、JITコンパイラを有効にしている場合にはバイトコードを最適化してくれます
何回も呼ばれている処理はホットスポットとして検出して、YARVバイトコードをx86/ARM機械語に変換します。以降は機械語を直接実行することで、大幅に高速化されます
そして、Ruby 3.1以降ではYJITというオプションで有効にできるJITコンパイラがあり、実行時にバイトコードを機械語に変換して高速化します
Ruby 3.3以降では、RJITというMJITの後継となる新しいJITコンパイラがあります。MJITは実行時にCコンパイラが必要で、YJITはビルド時にRustコンパイラが必要ですが、RJITはどちらも不要です












