Rubyのしくみ Ruby Under a Microscope 読書メモ
https://scrapbox.io/files/66586885766399001d2471b6.png
この本はずっと読みたかったし、落ち着いて読む時間が取れて本当に幸せである。
楽しそう!だから幸せというのもあるし、エンジニアとして色々考えながらキャリアを積んでいき、まだ開発できていることにも幸せを感じる。読んでいこう📝
はじめに
Cの理解がなくても...という前置きがあったが、あるということはCに理解があった方がRubyを知るには良さそうなので、やはりどこかで勉強したい📝
MRI (Matz' Ruby Implementation)
C言語で実装されたRubyの公式処理系。
プログラミング言語における処理系とは、プログラムを実行可能な形に変換し、実行するシステムのこと。コンパイラ、インタプリタ、仮想マシンなど。
仮想マシンとは、物理的なハードウェアではなく、ソフトウェアによってエミュレートされたコンピュータシステムのこと。
1〜11章全部面白そう
特にパーサーとコンパイラ、仮想マシンの章はほとんど知見がないので楽しみすぎる
第1章 字句解析と構文解析
Rubyは3回コードを変換している!(知らんかった)
トークン列になって、ASTノードになって、YARV命令になる
まず、字句解析、次に構文解析、コンパイル
YARVの名前ずっと気になってた
YARV: Yet Another RubyVM の名前の由来を述べておく。開発当初(2004 年元旦)には、いくつかRuby の VM として知られていたが、しかし不完全であったり効率がよくないような実装がいくつか存在した。そのため、Yet Another という名前を選んだ。
そうだったんだ...
code:simple.rb
10.times do |n|
puts n
end
当然1行ずつ読んでいく。なるほど、ヒトだと1行だけだとわからないけど、どう読んでいくんだろう。
文字を1文字ずつ読むんだ
字句解析コードは1つのプロセスというよりも、複数回呼び出されている
10だったら、10まで読んで、ピリオドに気づく
でも、浮動小数点だとも思ってくれる
💬なるほど〜〜〜!
ので、1文字さらに進むらしい
そこで初めてtINTEGER 10というトークンを作る
識別子
予約語ではないコードの中の単語
RubyのCコードは内部に予約後の定数テーブルを持っている
parse.yの話出てきた
構文解析ルール
parser_yylex関数
code:parse.y
static enum yytokentype
parser_yylex(struct parser_params *p)
{
register int c;
int space_seen = 0;
int cmd_state;
int label;
enum lex_state_e last_state;
int fallthru = FALSE;
int token_seen = p->token_seen;
// 中略
💬すごい、本当にこの後に続くswitch文で1文字ずつ見ているんだ
lex_stateにその時のコードの状態や種類が入る
Rubyで雰囲気の実装してみた。こんな感じだろうか。
code:ruby
LEX_STATE_INITIAL = 0
LEX_STATE_IN_WORD = 1
LEX_STATE_IN_NUMBER = 2
def parser_yylex(input)
lex_state = LEX_STATE_INITIAL
tokens = []
buffer = ""
input.each_char do |char|
case lex_state
when LEX_STATE_INITIAL
lex_state = LEX_STATE_IN_WORD
buffer << char
lex_state = LEX_STATE_IN_NUMBER
buffer << char
end
when LEX_STATE_IN_WORD
buffer << char
else
tokens << { type: :WORD, value: buffer }
buffer = ""
lex_state = LEX_STATE_INITIAL
redo
end
when LEX_STATE_IN_NUMBER
buffer << char
else
tokens << { type: :NUMBER, value: buffer }
buffer = ""
lex_state = LEX_STATE_INITIAL
redo
end
end
end
case lex_state
when LEX_STATE_IN_WORD
tokens << { type: :WORD, value: buffer } unless buffer.empty?
when LEX_STATE_IN_NUMBER
tokens << { type: :NUMBER, value: buffer } unless buffer.empty?
end
tokens
end
input = "hello 123 world 456"
tokens = parser_yylex(input)
puts tokens.inspect
Ripper
ソースコードに対して生成するトークンを見ることができる
code: ruby
require 'ripper'
code = <<-RUBY
10.times do |i|
puts i
end
RUBY
parsed = Ripper.lex(code)
pp parsed
code: console
[[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, "i", 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, "i", END|LABEL], [2, 8, :on_nl, "\n", BEG], [3, 0, :on_kw, "end", END], 3, 3], :on_nl, "\n", BEG
行番号と列番号が出ている。1行目、何文字か。
lexical analysis(字句解析)
与えられたコードが「Rubyのコードとして正しいかどうか」はRipperは判断しない
シンタクスエラーとかも字句解析しちゃう
まだまだコード実行まではプロセスがある。字句解析の後は構文解析。
構文解析
Ripperで生成したトークン列を、パーサーで構文解析する
YACCがその一つで、その新しいバージョンがbison(RubyKaigiの話で置き換わった話があったやつ)
parse.yはRubyの構文と文法が定義されている!
💬 わかりやすい
bisonはparse.yからparse.cを生成していた(パーサーコード)
パーサーコードとは?
code:GPT4o
Bisonは文法の定義ファイル(通常、拡張子が .y のファイル)を入力として受け取り、その文法に従ってC言語やC++で書かれたパーサー(解析器)コードを生成します。この出力ファイルの名前は通常 parse.c となります。
parse.c の内容は、以下のような機能を含んでいます:
字句解析器(Lexical Analyzer)との連携:入力テキストをトークンに分割し、各トークンの種類を識別します。これは通常、Flexなどのツールで生成された字句解析器と連携して行います。
構文解析(Parsing):文法ルールに従ってトークンのシーケンスを解析し、構文木を構築します。文法ルールがどのように適用されるかを決定し、必要に応じてアクションコードを実行します。
エラー処理:構文エラーが発生した場合の処理を行います。エラーメッセージの表示や、エラーからの回復手段を提供します。
これにより、Bisonによって生成された parse.c は、指定された文法に基づいて入力テキストを解析し、その構造を理解するためのプログラムの一部として機能します。この解析プロセスは、コンパイラやインタープリタなどのプログラムで非常に重要な役割を果たします。
図のBisonによって生成されているparse.y語釈?cかな?
これがさっき字句解析されたトークン列を見てくれるらしい。
💬 面白い〜〜〜
ただ、parse.yやparse.c自体も字句解析コードは含むらしい
構文解析器である一方で、字句解析機でもある
という理解をした
LALR構文解析アルゴリズム
code:md
LALRの正式な英語は "Look-Ahead Left-to-Right, Rightmost Derivation"
これを日本語に訳すと、「先読み左から右への解析、右端導出」となります。
トークン列のストリームを左から右に処理していく
ストリームとは
配列に似ているが、サイズなどが違うデータ構造っぽい
🚃 寄り道:parserのライブラリがあるっぽい
自分で文法規則を作ってみる
code:ruby
require 'parslet'
class SimpleParser < Parslet::Parser
rule(:word) { match('a-zA-Z').repeat(1).as(:word) >> space? } rule(:space?) { match('\s').maybe }
rule(:sentence) { (word >> space?).repeat(1) }
root(:sentence)
end
class SimpleTranslator < Parslet::Transform
rule(word: 'I') { '私は' }
rule(word: 'like') { '好きです' }
rule(word: 'Ruby') { 'ルビー' }
end
puts 'Enter a sentence:'
input = gets.chomp
parser = SimpleParser.new
translator = SimpleTranslator.new
begin
parsed = parser.parse(input)
translated = translator.apply(parsed)
rescue Parslet::ParseFailed => error
end
# Enter a sentence:
# I like Ruby
# Translation: 私は 好きです ルビー
当然これだと固定値しか翻訳できないので、もっと柔軟に構文解析する必要がある
次: P16の規則マッチから