調べようと思った背景
RubyKaigi2024 に際して現地でより多くの収穫を得るために、言語処理 (CRuby) の前提知識を整理しておきたい。まずは Ruby コードがどのような流れを経て実行されるのか確認する。
参考資料:Rubyのしくみ -Ruby Under a Microscope-
概観
下記資料にて綺麗にまとまっていたので引用する。上段の「CRuby 実行環境」の流れを追っていく。
コード実行の流れ
字句解析
Ruby コードが実行されて最初に行われるのが字句解析。ただの文字列のかたまりであるコードに意味を与える。字句解析ではトークン列と呼ばれる「理解可能な単語の列」へと文字列を変換する。
構文解析
次に構文解析でトークン列から AST ノードを生成する。このステップでは、パーサコードがトークン列を解析し、Ruby が理解できる文やフレーズにグルーピングする。
コンパイル
構文解析で生成された AST ノードをバイトコードに変換する。バイトコードとは、YARV が解釈できる低級な命令列のこと。
YARV 命令実行
コンパイラが生成したバイトコードは YARV によって実行される。
サンプルコードで見てみる
字句解析、構文解析、コンパイルまでの流れを実際のコードで見てみる。
字句解析
Ripper.lex で Ruby コードをトークン列に分割し、そのリストを出力する。
require 'ripper' require 'pp' code = <<STR 10.times do |n| puts n end STR puts code pp Ripper.lex(code)
❯ ruby lex.rb 10.times do |n| puts n end [[[1, 0], :on_int, "10", END], [[1, 2], :on_period, ".", DOT], [[1, 3], :on_ident, "times", ARG], [[1, 8], :on_sp, " ", ARG], [[1, 9], :on_kw, "do", BEG], [[1, 11], :on_sp, " ", BEG], [[1, 12], :on_op, "|", BEG|LABEL], [[1, 13], :on_ident, "n", ARG], [[1, 14], :on_op, "|", BEG|LABEL], [[1, 15], :on_ignored_nl, "\n", BEG|LABEL], [[2, 0], :on_sp, " ", BEG|LABEL], [[2, 2], :on_ident, "puts", CMDARG], [[2, 6], :on_sp, " ", CMDARG], [[2, 7], :on_ident, "n", END|LABEL], [[2, 8], :on_nl, "\n", BEG], [[3, 0], :on_kw, "end", END], [[3, 3], :on_nl, "\n", BEG]]
10
.
times
のように文字列を分解し、数値を示す on_int
やメソッドや変数名を示す on_ident
などの種別を付与していることが確認できる。[n, m]
はトークンが現れるコード位置を示している。
まとめ: 字句解析は、文字列を意味のある単位に分解し、それぞれに種別をラベリングしている。
構文解析
Ripper は字句解析だけでなく構文解析の結果も出力できる。Ripper.sexp は Ruby コードを S 式のツリーにして出力する。S 式は木構造のデータ形式のことのようだ。余談だが、Ruby もアイデアを継承している Lisp では S 式をソースコードの表現としても使うらしい。
require 'ripper' require 'pp' code = <<STR 10.times do |n| puts n end STR puts code pp Ripper.sexp(code)
❯ ruby sexp.rb 10.times do |n| puts n end [:program, [[:method_add_block, [:call, [:@int, "10", [1, 0]], [:@period, ".", [1, 2]], [:@ident, "times", [1, 3]]], [:do_block, [:block_var, [:params, [[:@ident, "n", [1, 13]]], nil, nil, nil, nil, nil, nil], false], [:bodystmt, [[:command, [:@ident, "puts", [2, 2]], [:args_add_block, [[:var_ref, [:@ident, "n", [2, 7]]]], false]]], nil, nil, nil]]]]]
全て見ると大変なので一部を見てみる。
[[:command, [:@ident, "puts", [2, 2]], [:args_add_block, [[:var_ref, [:@ident, "n", [2, 7]]]], false]]]
[:@ident, "puts", [2, 2]]
のような配列が構文木の1つのノードを示している。args_add_block
は puts
の引数とブロックを受け付けるためのノードである。さらに変数 n
を参照するためのノード var_ref
をネストしている。
まとめ: 構文解析では、与えられたトークン列から構文木を生成しており、構文木は意味のあるまとまり(ノード)を作ってネストして連結することで表現される。
コンパイル
RubyVM::InstructionSequence#disasm で Ruby コードを YARV 命令列に変換した結果を見れる。
code = <<STR 10.times do |n| puts n end STR puts RubyVM::InstructionSequence.compile(code).disasm
❯ ruby disasm.rb == disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(3,3)> 0000 putobject 10 ( 1)[Li] 0002 send <calldata!mid:times, argc:0>, block in <compiled> 0005 leave == disasm: #<ISeq:block in <compiled>@<compiled>:1 (1,9)-(3,3)> local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] n@0<Arg> 0000 putself ( 2)[LiBc] 0001 getlocal_WC_0 n@0 0003 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE> 0005 leave ( 3)[Br]
上部がトップレベルで呼び出す 10.times
の命令列、下部がブロックで渡される puts n
の命令列になっている。異なるスコープは別々の命令列として実行されるらしい。どちらの命令列でもメッセージを送るオブジェクト(レシーバ)が初めに設定されていることが分かる。そして send
で オブジェクトに対してメッセージを送っている様子が見て取れる。最後の leave
は return 文を示している。
まとめ: コンパイルでは、YARV 命令列が生成される。レシーバの設定やメソッド呼び出しが命令として順番に並べられる。
一言
サンプルコードで処理過程を追うことで雰囲気を理解できた。Ruby 完全理解した。