Slide 1

Slide 1 text

Lrama へのコントリビューションを通して学ぶ Ruby のパーサージェネレーター事情 株式会社永和システムマネジメント アジャイル事業部 小林 純一 (@junk0612) 大阪 Ruby 会議 03 ハートンホテル心斎橋別館 松風ホール 2023/09/09(Sat.)

Slide 2

Slide 2 text

小林 純一 (@junk0612)

Slide 3

Slide 3 text

自己紹介 ● 小林純一 ● X / GitHub: @junk0612 ● 株式会社永和システムマネジメント アジャイル事業部 ○ RubyxAgile グループ ○ 構文解析器研究部員 ● 音ゲーマー・ボードゲーマー

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

本日のおしながき ● パーサー周りの基礎知識 ● Lrama にコントリビューションした話 ● 実装を通して知った Lrama の内部構造 ● 今後やってみたいこと

Slide 6

Slide 6 text

パーサー周りの基礎知識

Slide 7

Slide 7 text

パーサー周りの基礎知識 ● 構文解析器の構成要素 ○ レキサー ○ パーサー ○ パーサージェネレーター ● 言語処理系の用語 ○ 形式言語 ○ 文脈自由言語 ○ BNF

Slide 8

Slide 8 text

構文解析器の構成要素 ● レキサー ○ テキストを読み込んで、トークンに分割するプログラム ○ トークンに分割することをトークナイズという ● パーサー ○ トークン列を読み込んで、内部構造を取り出すプログラム ○ コンパイラ系では「ソースコード → AST」 ■ AST: Abstract Syntax Tree (抽象構文木) ○ JSON や CSV のパーサーでは「テキスト → データ構造」 ● パーサージェネレーター ○ 文法ファイルを読み込んで、パーサーを生成するプログラム

Slide 9

Slide 9 text

CRuby 実行環境 .rb ファイル Ruby VM 構文解析器の構成要素

Slide 10

Slide 10 text

CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード パーサー 生成器 構文解析器の構成要素

Slide 11

Slide 11 text

CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード パーサー 生成器 パーサージェネレーター 文法ファイル パーサー 構文解析器の構成要素

Slide 12

Slide 12 text

CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード パーサー 生成器 パーサージェネレーター 文法ファイル レキサー トークン列 AST パーサー 生成器 パーサー 構文解析器の構成要素

Slide 13

Slide 13 text

言語処理系の用語 ● 形式言語 ○ 「言語」を数学的・集合論的に取り扱う言語学の分野 ○ 言語がどのようなテキストとして表されるかを考える ■ 人間に意味のある言葉として認識されるかは気にしない ■ 英語なら「アルファベットの羅列で、たまにスペースや 記号が入る」など ○ 言語を構成する記号と、それらを結びつける文法からなる

Slide 14

Slide 14 text

言語処理系の用語 ● 文脈自由言語 (Context Free Grammar: CFG) ○ 形式言語の1つで、以下の形式で表されるもの ■ rule: A B C ... | D E F ... ■ このような記法を BNF(Backus-Naur Form) という ○ 世の中のほとんどのプログラミング言語が属する ○ パーサージェネレーターの入力となる文法ファイルに使われる ○ 他の文法によって展開できる記号を非終端記号という ○ それ以上展開できない記号を終端記号という

Slide 15

Slide 15 text

文脈自由言語と BNF number: digit digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

Slide 16

Slide 16 text

文脈自由言語と BNF number: digit digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 00 ~ 99 の2桁の数字全体を表す言語

Slide 17

Slide 17 text

文脈自由言語と BNF expression: digit '+' digit | digit '-' digit | digit '*' digit | digit '/' digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

Slide 18

Slide 18 text

文脈自由言語と BNF expression: digit '+' digit | digit '-' digit | digit '*' digit | digit '/' digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 1桁の数の2項四則演算式を表す言語

Slide 19

Slide 19 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

Slide 20

Slide 20 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression

Slide 21

Slide 21 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression / digit

Slide 22

Slide 22 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression ) / digit

Slide 23

Slide 23 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression + digit ) / digit

Slide 24

Slide 24 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + digit ) / digit

Slide 25

Slide 25 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + digit ) / digit

Slide 26

Slide 26 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + 7 ) / digit

Slide 27

Slide 27 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + 7 ) / 3

Slide 28

Slide 28 text

文脈自由言語と BNF expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 1桁のかっこ付き四則演算式を表す言語

Slide 29

Slide 29 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + 7 ) / 3

Slide 30

Slide 30 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + 7 ) / 3

Slide 31

Slide 31 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + digit ) / 3

Slide 32

Slide 32 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + digit ) / digit

Slide 33

Slide 33 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression + digit ) / digit

Slide 34

Slide 34 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression ) / digit

Slide 35

Slide 35 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression / digit

Slide 36

Slide 36 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression

Slide 37

Slide 37 text

実際のパーサーの処理 expression: digit | expression '+' digit | expression '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' Accepted!

Slide 38

Slide 38 text

まとめ ● ソースコードが実行・コンパイルされるとき、 レキサーとパーサーがプログラムを解析してデータ構造を作る ● パーサーを文法ファイルから自動生成するプログラムを パーサージェネレーターという ● パーサージェネレーターの入力に使われる文法ファイルは 文脈自由文法で書かれる ● 文脈自由文法の記法として BNF がある

Slide 39

Slide 39 text

Lrama に コントリビューションした話

Slide 40

Slide 40 text

やったこと ● Lrama に Named References を実装した

Slide 41

Slide 41 text

やったこと ● Lrama に Named References を実装した 🤔

Slide 42

Slide 42 text

Lrama とは ● Bison を代替するために作られた Ruby 製パーサージェネレーター ● RubyKaigi 2023 で kaneko.y さんが発表した ○ 詳しくは https://youtu.be/IhfDsLx784g?si=kO1q6mLpTa1bIRYL ● CRuby 3.3 から導入される ○ preview1 には入っているので、今この場でお試しいただけます ○ ※ Ruby のふるまいは変わりません

Slide 43

Slide 43 text

Lrama を導入するメリット ● Bison のバージョンに引きずられずに済む ○ Bison はユーザーごとにバージョンが異なるので、古いものがインス トールされていることを想定しなければいけない ○ 新しい機能が導入されても使えない ● Ruby 独自の実装ができる ○ LSP 向けに書きかけのコードをいい感じにパースする ○ 複雑になっている文法のパースをうまくやる

Slide 44

Slide 44 text

Lrama が目指すところ ● Usability ● Maintainability ● Universal Parser ● 詳しくは RubyKaigi 2023 での kaneko.y さんの発表をどうぞ ○ https://youtu.be/IhfDsLx784g?si=kO1q6mLpTa1bIRYL

Slide 45

Slide 45 text

Usability ● Error-tolerant なパーサーがほしい ○ 旧来のパーサーは、入力されたコードが文法的に正しいか どうかを判断すればよかった ○ LSP が存在している現代においては、書きかけの半端なコード についてもなるべくパースしたいという要求がある ■ Bison の panic モードでは文法エラーの場所を 読み飛ばしてしまうので困る

Slide 46

Slide 46 text

Maintainability ● 歴史的経緯により、parse.y は非常に複雑になっている ○ 特に、ここから生成される CRuby のパーサーとレキサーが 密結合しているのがメンテナンス性に悪影響を与えている ● パーサーの力をうまく使ってやるともっとメンテナンス しやすくなるので、それを使うための機能を入れたい

Slide 47

Slide 47 text

Universal Parser ● 世の中にはさまざまな Ruby 実装があり、 それぞれ独自のパーサーを利用している ○ mruby や JRuby といった別実装 ○ sorbet や typeprof のようなツール ● 現在の CRuby のパーサーは CRuby 独自の機能に依存しており 移植が難しくなっている ● 文法ファイルを整理することで、他実装にも利用できるパーサーを 作りたい

Slide 48

Slide 48 text

Lrama 周辺のパーサー事情 ● YARP ○ Bison が作ったパーサーを置き換えるべく作られている CRuby 向けの手書き (!) パーサー ○ JRuby / TruffleRuby での実績もあるらしい ● Bison ○ GNU が開発した Yacc の後継のパーサージェネレーター ○ CRuby においては、parse.y を読み込んで Ruby のパーサーを 出力するために使われていた ● Racc ○ 青木峰郎さんの開発したパーサージェネレーター ○ parser gem (RuboCop の依存 gem) などで使われている

Slide 49

Slide 49 text

Q: Racc で Bison の置き換えってできないの? 生成アルゴリズムは同じだが、下記の部分が異なっているために共通で利用 できる部分が少なく、イチから作ったほうが手間が少ない ● 入力する文法ファイルそのものの文法 ○ Bison は Yacc 由来、Racc は独自記法 ● 出力するパーサーの言語 ○ Bison は C、Racc は Ruby

Slide 50

Slide 50 text

Named References とは ● Bison の機能の 1 つ ● Action 内の References として非終端記号名を利用できる

Slide 51

Slide 51 text

Named References とは ● Bison の機能の 1 つ ● Action 内の References として非終端記号名を利用できる 🤔

Slide 52

Slide 52 text

%{ Prologue (約 1500 行) %} Bison declarations (約 200 行) %% Grammar rules (約 4500 行) %% Epilogue (約 8300 行) Bison 文法ファイルの構造 かっこ内は CRuby の parse.y における行数

Slide 53

Slide 53 text

%{ Prologue (約 1500 行) %} Bison declarations (約 200 行) %% Grammar rules (約 4500 行) ← 今回はここの話 %% Epilogue (約 8300 行) Bison 文法ファイルの構造

Slide 54

Slide 54 text

Grammar rules の構造 rule_name: rule rule .. rule { action } | rule rule .. rule { action } expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 }

Slide 55

Slide 55 text

Grammar rules の構造 rule_name: rule rule .. rule { action } | rule rule .. rule { action } expression: NUMBER '+' expression { $$ = $1 + $3 } ← | NUMBER '-' expression { $$ = $1 - $3 } ← 今回はここの話 | '(' expression ')' { $$ = $2 } ←

Slide 56

Slide 56 text

Action とは ● Bison が生成するパーサーは、なにもしなければ 文法に則った入力になっているかどうかしか教えてくれない ○ AST を作ったり、後の処理に必要な情報を保存したりはしない ● 各文法の後ろに {} でくくってプログラムを書くことができる ● $n や @n を使うと、文法中の記号の値を使って処理が行える ○ この機能を (Numbered) References という

Slide 57

Slide 57 text

Action の具体例 expression: NUMBER '+' expression { $$ = $1 + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算

Slide 58

Slide 58 text

Action の具体例 expression: NUMBER '+' expression { $$ = $1 + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算

Slide 59

Slide 59 text

Action の具体例 expression: NUMBER '+' expression { $$ = $1 + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算

Slide 60

Slide 60 text

Action の具体例 expression: NUMBER '+' expression { $$ = $1 + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算

Slide 61

Slide 61 text

Named References とは ● Numbered References の問題点 ○ 記述的でなくわかりにくい ○ 場所の番号で指定するため、 文法が変更になると書き直さなければならない ● 上記の問題を解消するため、非終端記号名を参照して 値を使えるようにした機能が Named References

Slide 62

Slide 62 text

Named References とは expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] }

Slide 63

Slide 63 text

Named References とは expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] } 前に $ をつけてルール名で値を参照できる

Slide 64

Slide 64 text

Named References とは expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] } ルール記述側で [] でくくると別名をつけられる

Slide 65

Slide 65 text

Named References とは expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] } ルール名や別名に記号を含む場合は 呼び出し側も [] でくくれば OK

Slide 66

Slide 66 text

Named References とは expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] }

Slide 67

Slide 67 text

ここまでのまとめ ● やったことの説明 ○ Lrama に Named References を実装した ■ Lrama は Bison を置き換えるために作られた パーサージェネレーター ■ Named References は Bison が持つ機能で、Action 内の References として非終端記号名を利用できる

Slide 68

Slide 68 text

実装を通して知った Lrama の内部構造

Slide 69

Slide 69 text

パーサージェネレーターの内部構造 パーサージェネレーター 文法ファイル レキサー トークン列 AST パーサー 生成器 パーサー

Slide 70

Slide 70 text

Lrama の内部構造 Lrama parse.y Lexer Token Grammar Parser Output パーサー

Slide 71

Slide 71 text

まずはテストを書く

Slide 72

Slide 72 text

どこに手を入れるか? ● Numbered References はすでに実装されている ○ 非終端記号名と Action 内の呼び出しを関連づけられれば 関連からコードを生成する部分はすでにある ○ Numbered References の実装を参考にして 非終端記号名と呼び出しを関連づけることにする

Slide 73

Slide 73 text

どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー

Slide 74

Slide 74 text

どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー

Slide 75

Slide 75 text

どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー

Slide 76

Slide 76 text

どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー

Slide 77

Slide 77 text

なぜレキサーに手を入れるのか ● Lrama ではレキサーが文脈を持ってトークナイズしている ○ 「いま何をパースしているか」という情報 ● Action を読み込むとき、直前に読んだルールの場所を確認して 「どのルールを参照しているか」という関連づけまでやっている ● これをまるごとパーサーに持っていくのは非常に大変だし、 もともとの設計にも合わない ○ トークナイズ → パースというフェーズ分けをしたかった

Slide 78

Slide 78 text

文脈をどこに持つか? ● レキサーは文脈を持っていないと適切なトークナイズができない ○ コメントは読み飛ばす ○ HereDoc の途中で改行が含まれてもパースエラーにしない ○ etc. ● これをパーサーに持つかレキサーに持つか

Slide 79

Slide 79 text

文脈をどこに持つか? ● レキサーに持つと ○ 入力から自身の状態を変えながら一気にトークナイズができ、 トークナイズ → パースというフェーズ分けができる ● パーサーに持つと ○ パーサーが状態を元に次のトークンをレキサーから受け取る ようになるので、レキサーはトークナイズに集中できる

Slide 80

Slide 80 text

https://github.com/ruby/lrama/pull/41 Pull Request

Slide 81

Slide 81 text

今後やりたいこと ● 内部パーサーの Racc による自動生成化 ○ WIP の PR → https://github.com/ruby/lrama/pull/62 ● Bison の機能の取り込み ● kaneko.y さんによるやりたいことリスト → https://docs.google.com/document/d/1EAZzYMXBOdzK-6mMIj 2YNJxZZRVcpJxE7-4zXbHn8JA/edit?usp=sharing

Slide 82

Slide 82 text

世はまさに 大パーサー時代 人々はパーサーの 海へと漕ぎ出す ――RubyKaigi 2023 LT kaneko.y